diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81c94041..770c2f7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,16 +53,19 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Build image + - name: Build backend image run: docker build -t website-profiling:ci . + - name: Build web image + run: docker build -t website-profiling-web:ci ./web --build-arg VITE_BFF_BASE_URL=http://localhost:8090 - name: Browser crawl tests in image run: | docker run --rm \ website-profiling:ci \ /opt/venv/bin/pytest tests/test_crawl_fetchers.py tests/test_crawler_browser_e2e.py -m browser -q -o addopts= - - name: Compose smoke (postgres + web) + - name: Compose smoke (postgres + fastapi + web) env: - WEB_IMAGE: website-profiling:ci + BACKEND_IMAGE: website-profiling:ci + WEB_IMAGE: website-profiling-web:ci run: | docker compose -f docker-compose.pull.yml up -d --wait curl -fsS http://127.0.0.1:3000/home @@ -82,6 +85,8 @@ jobs: cache-dependency-path: web/package-lock.json - name: Install run: npm ci + - name: Build + run: npm run build - name: Typecheck run: npm run typecheck - name: Lint @@ -98,3 +103,30 @@ jobs: dotnet-version: '10.0.x' - name: Test FileService run: dotnet test services/FileService/FileService.slnx + + bff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + - name: Test BFF + run: dotnet test services/Bff/Bff.slnx + - name: Generated client drift gate + run: | + dotnet tool install -g NSwag.ConsoleCore + export PATH="$PATH:$HOME/.dotnet/tools" + (cd services/Bff && nswag run nswag.json) + git diff --exit-code services/Bff/src/Bff.Application/Generated/FastApiClient.g.cs \ + || (echo "::error::FastApiClient.g.cs is stale — run services/Bff/generate-client.sh and commit." && exit 1) + + data: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + - name: Test Data service + run: dotnet test services/Data/Data.slnx diff --git a/.gitignore b/.gitignore index 5ea03af9..503b94b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -# Project (root) only. Python: src/.gitignore. Next.js: web/.gitignore. .NET: services/FileService/ +# Project (root) only. Python: src/.gitignore. Web UI: web/.gitignore. .NET: services/*/ # Next.js UI: generated pipeline configs from the runner modal (repo root; must match Python cwd for paths) .website-profiling-ui-*.txt @@ -33,12 +33,13 @@ pipeline-config.txt skills-lock.json crawl_results.csv commit.* - -# .NET FileService — build output and IDE artifacts -services/FileService/**/bin/ -services/FileService/**/obj/ -services/FileService/.vs/ -services/FileService/**/*.user -services/FileService/**/*.suo -services/FileService/**/TestResults/ -services/FileService/**/*.DotSettings.user \ No newline at end of file +.cursor/ +.claude/ +# .NET services (FileService, Bff, …) — build output and IDE artifacts +services/**/bin/ +services/**/obj/ +services/**/.vs/ +services/**/*.user +services/**/*.suo +services/**/TestResults/ +services/**/*.DotSettings.user diff --git a/AGENT.md b/AGENT.md index e51865f6..fcd3c030 100644 --- a/AGENT.md +++ b/AGENT.md @@ -6,16 +6,17 @@ Developer reference for agents and contributors. User-facing overview: [README.m **LLM / AI:** Settings live in **`llm_config`** table in PostgreSQL. Providers: OpenAI, Google Gemini, Anthropic, Groq, Ollama (`web/src/lib/llmConfigSchema.ts`). Configure only via web UI **AI** tab (`GET/PUT /api/llm-config`, localhost). Never in `pipeline-config.txt` or `--config`. -**Frontend:** **`web/`** (Next.js) -- server reads PostgreSQL via `/api/report/*`. +**Frontend:** **`web/`** (Vite + React SPA) — browser calls **`services/Bff/`** for all `/api/*`; BFF proxies to FastAPI and FileService. **Key paths** - `src/website_profiling/` -- `cli.py`, `config.py`, `crawl/`, `db/storage.py`, `lighthouse/`, `reporting/`, `analysis/`, `llm/`, `tools/` +- `services/Bff/` -- .NET BFF (auth, CORS, `/api/*` proxy) - `services/FileService/` -- .NET PDF + Excel workbook export (HTTP-only; see [README](services/FileService/README.md)) -- `web/app/` -- routes; `web/src/` -- React; pipeline: `PipelineRunnerFab`, `server/pipelineJobs.ts`, `server/pipelineConfig.ts`, `server/llmConfig.ts`, `server/db.ts` +- `web/src/` -- React SPA (`AppRoutes.tsx`, `views/`, `components/`); pipeline UI: `PipelineRunnerFab`, `PipelineContext` - `alembic/` -- schema migrations -**Local dev:** `./local-run` (Postgres in Docker `wp-pg`, FileService on `:8080`, Next.js on host; default `DATABASE_URL`: `postgres://postgres:dev@127.0.0.1:5432/website_profiling`). See `scripts/local-run.sh`. **Local tests:** `./local-test` runs **three** Python coverage gates (core 100%, reporting 100%, tools 100%) plus web checks — mirrors CI **python** and **web** jobs; Docker CI is separate (see `.github/workflows/ci.yml`). `./local-test browser` for `@pytest.mark.browser` integration tests — see `scripts/local-test.sh`. Mocked browser unit tests: `tests/test_browser_fetcher_unit.py`. +**Local dev:** `./local-run` (Postgres in Docker `wp-pg`, FileService on `:8080`, FastAPI on `:8001`, BFF on `:8090`, Vite on `:3000`; default `DATABASE_URL`: `postgres://postgres:dev@127.0.0.1:5432/website_profiling`). See `scripts/local-run.sh`. **Local tests:** `./local-test` runs **three** Python coverage gates (core 100%, reporting 100%, tools 100%) plus web checks — mirrors CI **python** and **web** jobs; Docker CI is separate (see `.github/workflows/ci.yml`). `./local-test browser` for `@pytest.mark.browser` integration tests — see `scripts/local-test.sh`. Mocked browser unit tests: `tests/test_browser_fetcher_unit.py`. **JavaScript crawl (optional):** Config keys `crawl_render_mode` (`static` | `javascript` | `auto`) and `crawl_js_*` in pipeline config / `pipelineConfigSchema.ts`. JS/auto crawls can capture browser console errors and uncaught exceptions (`crawl_js_capture_console`, stored under `page_analysis.browser`). **Auto mode** uses static-first fetch, pre-parse SPA heuristics (`needs_js_render`), then post-parse low-outlink fallback (`needs_js_render_after_parse`) in `crawler.py`. **Preflight:** `GET /api/crawl/browser-status` (localhost) spawns Python `browser_status()`; Run audit settings/run validation calls it when render mode is `javascript` or `auto`. Browser deps: Playwright from `requirements.txt` (installed by `./local-run setup` and `./local-test`). Runtime needs Chromium on `PATH` or `CHROME_PATH` (Docker sets `CHROME_PATH=/usr/bin/chromium`). Integration tests: `@pytest.mark.browser` — excluded by default in `pytest.ini`; Docker CI runs `tests/test_crawl_fetchers.py` and `tests/test_crawler_browser_e2e.py -m browser`; locally `./local-test browser`. @@ -26,13 +27,13 @@ Developer reference for agents and contributors. User-facing overview: [README.m - **`preserve_crawl_history`** (default true): append crawls; `false` truncates crawl tables but restores `report_payload`, Lighthouse, `google_data`, `keyword_data`, `keyword_history`, `keyword_suggest_cache`, and `crawl_runs` - **`DATABASE_URL`** env: PostgreSQL connection string (required). **`DATA_DIR`**: secrets + shadow config (Docker: `/data`). - **Pipeline storage** (crawl, edges, nodes, report payload, Lighthouse, keywords, warnings) lives in **PostgreSQL only**. Deliverables use the Export view, `GET /api/report/export`, or MCP `export_*` tools — not files written by the main pipeline step. -- **Pool tuning:** `DB_POOL_MIN` / `DB_POOL_MAX` (Python), `PGPOOL_MAX` (Node). Bulk crawl writes via `executemany`; optional **`crawl_stream_to_db`** streams rows during fetch. Per-URL raw HTML: `crawl_page_html` table (migration `015`); API `GET/POST /api/crawl/page-html` (localhost). -- **`web/` APIs:** `/api/report/*` read routes (payload, meta, history — not localhost-guarded; protect with `AUTH_*` when exposed); `/api/run` spawns Python (localhost); `/api/jobs`, `/api/jobs/[id]`, `/api/jobs/[id]/cancel` (localhost); `/api/crawl/browser-status`, `/api/crawl/page-html` (localhost); `/api/pipeline-config` GET/PUT; `/api/llm-config` GET/PUT; `/api/chat` POST (SSE); `/api/chat/sessions` GET/POST; `/api/ollama/status` (localhost); `/api/properties/{id}/google/links/import` POST; `PipelineRunnerFab` saves pipeline + LLM state before each run. Full route list: `web/app/api/**/route.ts`. +- **Pool tuning:** `DB_POOL_MIN` / `DB_POOL_MAX` (Python). Bulk crawl writes via `executemany`; optional **`crawl_stream_to_db`** streams rows during fetch. Per-URL raw HTML: `crawl_page_html` table (migration `015`); API `GET/POST /api/crawl/page-html`. +- **Browser API (BFF):** All `/api/*` routes are served by `services/Bff/` (proxied to FastAPI / FileService). Notable: `/api/report/*`, `/api/run`, `/api/jobs/*`, `/api/pipeline-config`, `/api/llm-config`, `/api/chat` (SSE), `/api/integrations/google/*` (OAuth callback on BFF origin). `PipelineRunnerFab` saves pipeline + LLM state before each run. OpenAPI: `web/openapi.json`; BFF client: `services/Bff/src/Bff.Application/Generated/`. - **MCP:** `python -m website_profiling.mcp` (stdio) or `python -m website_profiling.mcp.http` (remote Streamable HTTP). Configure at **`/mcp`** in the web UI. See `docs/MCP.md`. - **AI Chat UI:** `/chat` — property-scoped chat with saved sessions (`chat_sessions`, `chat_messages`; migration `012_chat_sessions`). -- **Job store:** PostgreSQL `pipeline_jobs` when `DATABASE_URL` is set (`pipelineJobsDb.ts` — status, timestamps, truncated logs). In-memory map in `pipelineJobs.ts` holds live log tail and child process handles; stale rows reconciled via `PIPELINE_JOB_STALE_HOURS`. +- **Job store:** PostgreSQL `pipeline_jobs` (FastAPI); live job status via `/api/jobs/*` through the BFF. - **Schema head:** `015_crawl_page_html` (recent: `013` link_edges/discovery, `014` job log truncation, `015` per-URL HTML storage). -- **Docker:** `Dockerfile` + `docker-compose.yml` (postgres + web + FileService); **`docker-compose.prod.yml`** (production + remote MCP on `:8000`); **`docker-compose.pull.yml`** for pre-built images (`WEB_IMAGE`); **`LIGHTHOUSE_CHROME_FLAGS`** +- **Docker:** Root `Dockerfile` (Python backend); `web/Dockerfile` (Vite SPA + nginx); `docker-compose.yml` (postgres + fastapi + worker + bff + web + FileService); **`docker-compose.prod.yml`** (production + optional MCP on `:8000`); **`docker-compose.pull.yml`** for pre-built images (`BACKEND_IMAGE`, `WEB_IMAGE`); **`LIGHTHOUSE_CHROME_FLAGS`** **Where to edit** @@ -40,7 +41,7 @@ Developer reference for agents and contributors. User-facing overview: [README.m |------|--------| | Crawl | `crawl/crawler.py`, `crawl/fetchers/` | | Report | `reporting/builder.py`, `reporting/categories.py` | -| PDF / workbook export | `services/FileService/` (rendering); Next.js proxies in `web/src/server/proxyToFileService.ts` | +| PDF / workbook export | `services/FileService/` (rendering); BFF routes `/api/report/export` and `/api/report/export-workbook` to FileService | | DB schema | `alembic/versions/` | | Local analysis | `analysis/local.py`, `requirements.txt` | | AI insights (LLM) | `llm/enrich.py`, `llm/agent.py`, `llm_config.py`, `requirements.txt` | @@ -49,7 +50,7 @@ Developer reference for agents and contributors. User-facing overview: [README.m | Config / CLI | `config.py` (`load_config`, `load_config_from_db`), `cli.py`, `input.txt.example` | | UI pipeline schema | `web/src/lib/pipelineConfigSchema.ts` | | UI LLM schema | `web/src/lib/llmConfigSchema.ts` | -| UI config I/O | `web/src/server/pipelineConfig.ts`, `web/src/server/llmConfig.ts` | +| Browser API client | `web/src/lib/publicBase.ts` (`apiUrl`, `apiFetch`, `VITE_BFF_BASE_URL`) | | D3 charts (custom / compare / overview) | `web/src/components/charts/d3/`, `web/src/lib/viz/` | | Chart.js charts (standard bar/line/doughnut) | `web/src/utils/chartJsDefaults.ts`, `react-chartjs-2` in views under `web/src/views/`, `web/src/components/searchPerformance/`, `web/src/components/traffic/` | @@ -86,7 +87,7 @@ The web UI uses **both** Chart.js and D3.js. Pick the library that fits each cha - Keep chart-library types out of data-prep: use neutral shapes (`BarChartData`, `DualSeriesChartData` in `web/src/lib/viz/types.ts` and `web/src/lib/compareChartData.ts`); convert at the render layer via `web/src/lib/viz/adapters.ts` when needed. - Migrate page-by-page when D3 is the better fit; do not remove `chart.js` from `package.json` until all consumers are migrated. -**Company standards:** UI copy in `web/src/strings.json` (Site Audit, Properties, Run audit). Data provenance on `report_meta` in report payload. Docs: `docs/COMPANY_STANDARDS.md`, `docs/GLOSSARY.md`. Migration `003_company_standards` (properties, pipeline_jobs, audit_log). Durable jobs in `web/src/server/pipelineJobsDb.ts`. **Export:** PDF/workbook via FileService (`FILE_SERVICE_URL` on web/MCP; `REPORT_API_URL` on FileService); CSV/JSON via `GET /api/report/export` and `src/website_profiling/tools/export_audit.py`. +**Company standards:** UI copy in `web/src/strings.json` (Site Audit, Properties, Run audit). Data provenance on `report_meta` in report payload. Docs: `docs/COMPANY_STANDARDS.md`, `docs/GLOSSARY.md`. Migration `003_company_standards` (properties, pipeline_jobs, audit_log). **Export:** PDF/workbook via FileService (`FILE_SERVICE_URL` on MCP; `REPORT_API_URL` on FileService); CSV/JSON via `GET /api/report/export` and `src/website_profiling/tools/export_audit.py`. **Common footguns (check before finishing web or DB work)** @@ -94,15 +95,16 @@ These recur when adding features. Verify explicitly — do not assume tests caug 1. **React context — `useReport` / `ReportProvider`** - Report views call `useReport()`. That only works inside `ReportAppClient` → `ReportProvider`. - - **Do:** Render report views via `ReportShell` (wraps `ReportAppClient` internally). - - **Don't:** Import a view directly in `app/*/page.tsx` without `ReportShell`. - - Standalone routes under `web/app/` (e.g. `log-analyzer`, `indexation`) are **not** auto-wrapped by `(reports)/layout`. + - **Do:** Render report views via `ReportShell` inside `ReportLayout` (`AppRoutes.tsx` → `/:slug`). + - **Don't:** Mount a report view outside `ReportAppClient` / `ReportProvider`. + - Standalone routes (`/pipeline`, `/chat`, `/write`, etc.) are defined in `web/src/AppRoutes.tsx`, not wrapped by `ReportLayout`. ```tsx - // ✅ + // ✅ ReportSlugPage in web/src/pages/ReportSlugPage.tsx import ReportShell from '@/ReportShell'; - export default function Page() { - return ; + export default function ReportSlugPage() { + const { slug } = useParams(); + return ; } ``` diff --git a/AGENTS.md b/AGENTS.md index be63f52f..797c472b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,14 +4,16 @@ This file is the canonical entry point for agents. For full detail see [AGENT.md](AGENT.md). -**What it is:** Self-hosted SEO crawl and technical audit platform — `python -m src` from repo root. Stack: Python (crawl + analysis + MCP), Next.js (web UI), PostgreSQL. +**What it is:** Self-hosted SEO crawl and technical audit platform — `python -m src` from repo root. Stack: Python (crawl + analysis + MCP + FastAPI), Vite + React SPA (web UI), .NET BFF (browser API), .NET Data (report reads), PostgreSQL. **Key paths** - `src/website_profiling/` — core Python package - - `cli.py`, `config.py`, `crawl/`, `db/`, `reporting/`, `analysis/`, `llm/`, `tools/` -- `web/` — Next.js frontend -- `services/FileService/` — .NET PDF + Excel workbook export (port 8080). HTTP-only via `REPORT_API_URL`; no Postgres. Profiles: `executive|standard|full|premium`. Details: [services/FileService/README.md](services/FileService/README.md). Env: `FILE_SERVICE_URL` (Next.js/MCP), `REPORT_API_URL` (FileService). + - `cli.py`, `config.py`, `api/`, `worker/`, `crawl/`, `db/`, `reporting/`, `analysis/`, `llm/`, `tools/` +- `web/` — Vite + React SPA (static nginx in prod); browser calls `services/Bff/` for all `/api/*` +- `services/Bff/` — .NET BFF (auth, CORS, proxy to FastAPI + Data + FileService) +- `services/Data/` — .NET read service (report payloads, portfolio, issue status, filters; port 8091) +- `services/FileService/` — .NET PDF + Excel workbook export (port 8080). HTTP-only via `REPORT_API_URL`; no Postgres. Profiles: `executive|standard|full|premium`. Details: [services/FileService/README.md](services/FileService/README.md). Env: `FILE_SERVICE_URL` (MCP), `REPORT_API_URL` (FileService). - `alembic/` — DB migrations - `docs/` — documentation index - `tests/` — pytest suite @@ -19,8 +21,8 @@ This file is the canonical entry point for agents. For full detail see [AGENT.md **Run / dev** ```bash -./local-run # Start Postgres + FileService + Next.js -./local-test # Run all three coverage gates +./local-run # Start Postgres + FileService + Data + worker + FastAPI + BFF + Vite dev server +./local-test # Python + web + .NET tests (CI parity) python -m src # Run audit pipeline python -m website_profiling.mcp # Start MCP server (stdio) ``` @@ -35,7 +37,7 @@ python -m website_profiling.mcp # Start MCP server (stdio) | Report | `src/website_profiling/reporting/` | | GEO / AEO / Agent readiness | `src/website_profiling/tools/audit_tools/geo/geo_tools.py`, `geo/agent_readiness.py` | | DB schema | `alembic/versions/` | -| UI | `web/src/views/`, `web/app/` | +| UI | `web/src/views/`, `web/src/pages/`, `web/src/AppRoutes.tsx` | | Charts | D3: `web/src/components/charts/d3/`, `web/src/lib/viz/` · Chart.js: GSC/GA4/Links etc. — see [AGENT.md](AGENT.md) § Charts | **Charts:** Use **both** Chart.js and D3 — choose per chart (Overview/Compare → D3; standard GSC/GA4 bars → Chart.js). Full rules in [AGENT.md](AGENT.md). diff --git a/Dockerfile b/Dockerfile index 94d185bd..ca600ca7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,6 @@ # syntax=docker/dockerfile:1 -# WebsiteProfiling: Next.js UI + FastAPI (port 8001) + Python worker + pipeline. +# WebsiteProfiling: FastAPI (port 8001) + Python worker + pipeline. +# Web UI is a separate image: web/Dockerfile (Vite SPA + nginx). # Build from repository root: docker build -t website-profiling . # BuildKit cache mounts (default in Docker Desktop) reuse pip/npm downloads across rebuilds. @@ -32,7 +33,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ - NEXT_TELEMETRY_DISABLED=1 \ WEBSITE_PROFILING_ROOT=/app \ DATA_DIR=/data \ PYTHON=/opt/venv/bin/python \ @@ -57,27 +57,19 @@ RUN --mount=type=cache,target=/root/.npm \ WORKDIR /app -# Next.js install + build (layer cache) -COPY web/package.json web/package-lock.json /app/web/ -RUN --mount=type=cache,target=/root/.npm \ - cd /app/web && npm ci - # Application source COPY pytest.ini /app/pytest.ini COPY src /app/src COPY tests /app/tests -COPY web /app/web COPY alembic /app/alembic COPY alembic.ini /app/alembic.ini COPY docker-entrypoint.sh /app/docker-entrypoint.sh -RUN cd /app/web && npm run build && npm prune --omit=dev - ENV NODE_ENV=production # Persisted data directory (secrets + shadow config) RUN mkdir -p /data && chmod +x /app/docker-entrypoint.sh -EXPOSE 3000 +EXPOSE 8001 CMD ["/app/docker-entrypoint.sh"] diff --git a/README.md b/README.md index 45b47f6f..070bfd7f 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@

- Next.js + React + Vite Python .NET PostgreSQL @@ -40,7 +41,7 @@ # Site Audit -**Developer-friendly SEO audit platform** — open-source crawl and technical audit tooling built with **Next.js, Python, and PostgreSQL**. +**Developer-friendly SEO audit platform** — open-source crawl and technical audit tooling built with **React, Python, PostgreSQL, and .NET**. The stack is split into focused services: a **Python FastAPI** backend (crawl, pipeline, chat, integrations), a **.NET BFF** as the browser-facing API gateway, a **.NET Data** read service (report payloads, portfolio, issue status), and a **.NET FileService** for PDF/Excel export. ## Overview @@ -139,12 +140,23 @@ Also included: **AI chat** over audit data (optional), **Content studio** (write ## Architecture +```text +Browser → web (:3000) → bff (:8090) → fastapi (:8001) crawl, pipeline, chat, integrations + │ data (:8091) report reads, portfolio, issue status, filters + │ files (:8080) PDF + Excel export + worker background pipeline jobs (same Python image) + postgres audit data store +``` + ``` WebsiteProfiling/ ├── src/website_profiling/ # Python audit engine (CLI: python -m src) +│ ├── api/ # FastAPI app (uvicorn :8001) +│ ├── worker/ # Background pipeline job runner │ ├── crawl/ # Crawler, fetchers, JS rendering │ ├── reporting/ # Report builder, issue categories │ ├── analysis/ # On-page / local analysis +│ ├── content_studio/ # Content writing + live SEO scoring │ ├── lighthouse/ # Lighthouse runner │ ├── integrations/ # Google Search Console, GA4, Bing, CrUX │ ├── llm/ # AI enrich + chat agent @@ -154,23 +166,26 @@ WebsiteProfiling/ │ ├── commands/ # CLI subcommands │ ├── cli.py # Pipeline entrypoint │ └── config.py # Config load (DB + shadow file) -├── web/ # Next.js UI -│ ├── app/ # App Router pages + /api routes +├── web/ # Vite + React SPA (nginx in prod) +│ ├── src/AppRoutes.tsx # React Router routes │ ├── src/components/ # React UI components │ ├── src/views/ # Report views (overview, links, issues, …) -│ ├── src/server/ # Server-side DB, pipeline jobs, config I/O +│ ├── src/lib/ # Client helpers, BFF apiUrl/apiFetch │ └── public/ # Static assets (logo, favicon) +├── services/Bff/ # .NET BFF — auth + /api/* proxy (port 8090) +├── services/Data/ # .NET read service — report/portfolio/issue reads (port 8091) ├── services/FileService/ # .NET PDF + Excel workbook export (port 8080) ├── alembic/versions/ # PostgreSQL schema migrations ├── tests/ # pytest suite + fixtures ├── docs/ # Glossary, MCP, ops, brand assets -├── scripts/ # local-run.sh, local-test.sh helpers -├── .github/workflows/ # CI (Python + web + browser crawl) -├── docker-compose.yml # Dev stack (Postgres + web + FileService) -├── docker-compose.prod.yml # Production stack (requires AUTH_SECRET) -├── docker-compose.pull.yml # Pre-built WEB_IMAGE -├── Dockerfile # Production image +├── scripts/ # local-run.sh, local-test.sh, local-prod.sh +├── .github/workflows/ # CI (Python, web, .NET, Docker) +├── docker-compose.yml # Full dev stack (see Getting started) +├── docker-compose.prod.yml # Production stack (requires AUTH_SECRET; optional MCP profile) +├── docker-compose.pull.yml # Pre-built BACKEND_IMAGE + WEB_IMAGE smoke layout +├── Dockerfile # Python backend image (fastapi + worker roles) ├── local-run # Dev setup & start script +├── local-prod # Production build + preview (no hot reload) ├── local-test # Full test suite (CI parity) ├── requirements.txt # Python dependencies └── pipeline-config.example.txt @@ -180,8 +195,11 @@ WebsiteProfiling/ | Path | Purpose | | ------------------------------------- | ------------------------------------------------------------------------------ | | `src/website_profiling/` | Crawl, analyze, report, Lighthouse, integrations, AI — run via `python -m src` | +| `src/website_profiling/api/` | FastAPI HTTP layer — pipeline, chat, integrations, crawl control | +| `services/Bff/` | Browser API gateway — auth, CORS, routes `/api/*` to FastAPI, Data, FileService | +| `services/Data/` | .NET read/mutation service for report payloads, portfolio, issue status, saved filters | | `services/FileService/` | PDF and Excel workbook export — see [services/FileService/README.md](services/FileService/README.md) | -| `web/app/api/` | REST APIs: report data, pipeline runs, chat (SSE), Google/Bing sync | +| `web/src/lib/publicBase.ts` | BFF base URL (`VITE_BFF_BASE_URL`) and `apiFetch` / `apiUrl` | | `web/src/lib/pipelineConfigSchema.ts` | Audit settings schema (UI ↔ PostgreSQL) | | `alembic/versions/` | Database migrations — run `./local-run migrate` | | `tests/` | Backend tests; `./local-test browser` for Playwright crawl integration | @@ -193,28 +211,42 @@ For layout details and common development patterns, see [AGENT.md](AGENT.md). ## Getting started +### Prerequisites + +| Tool | Used for | +| ---- | -------- | +| **Docker** | Postgres container (local dev) and full-stack compose | +| **Python 3.12+** | Audit engine, FastAPI, pipeline worker, tests | +| **Node 20+** | Vite + React SPA | +| **.NET SDK 10+** | BFF, Data, and FileService (required for `./local-run`; optional if you only use Docker) | + ### Docker -Build and run from source: +Build and run the full dev stack from source: ```bash docker compose up --build ``` -Open [http://localhost:3000/home](http://localhost:3000/home). PDF and workbook exports require the **FileService** container (`files`, port 8080). +Services: **postgres**, **fastapi** (`:8001`, internal), **worker**, **data** (`:8091`, internal), **bff** (`:8090`), **web** (`:3000`), **files** (`:8080`, internal). + +Open [http://localhost:3000/home](http://localhost:3000/home). The browser talks only to the **BFF** (`:8090`); the BFF proxies to FastAPI, the Data service (report reads and portfolio routes), and FileService (PDF/workbook export). -Production deployment: `docker-compose.prod.yml` — set `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `AUTH_SECRET`. Pre-built images: `docker-compose.pull.yml` (`WEB_IMAGE`). +Production deployment: `docker-compose.prod.yml` — set `POSTGRES_USER`, `POSTGRES_PASSWORD`, `AUTH_SECRET`, `BFF_ALLOWED_ORIGINS`, and `BFF_PUBLIC_URL`. Optional remote MCP: `docker compose -f docker-compose.prod.yml --profile mcp up`. Pre-built images: `docker-compose.pull.yml` (`BACKEND_IMAGE`, `WEB_IMAGE`). ### Local development ```bash -./local-run setup # First time: Postgres, Python venv, migrations, npm deps -./local-run # Start DB + FileService + Next.js → http://localhost:3000/home +./local-run setup # First time: Postgres, Python venv, Playwright/Chromium, migrations, npm deps +./local-run # Start full dev stack → http://localhost:3000/home ./local-run db # Postgres only (no app) ./local-run migrate # Apply Alembic migrations only ./local-run stop # Stop Postgres container +./local-prod # Same DB, Vite production build + preview (no hot reload) ``` +`./local-run` starts (in order): **FileService** `:8080`, **Data** `:8091`, **pipeline worker**, **FastAPI** `:8001`, **BFF** `:8090`, and **Vite** `:3000`. Use `localhost` (not `127.0.0.1`) for pipeline APIs so CORS and cookies match the BFF origin. + Default local `DATABASE_URL`: `postgres://postgres:dev@127.0.0.1:5432/website_profiling` (Docker Compose dev stack uses `profiling:profiling`). `requirements.txt` pins direct Python dependencies to versions verified by `./local-test python`. Re-run the full test suite after intentional upgrades. @@ -233,15 +265,16 @@ Increase `PIPELINE_JOB_STALE_HOURS` for crawls that routinely exceed one hour. ### Testing ```bash -./local-test # Python + web (matches CI python and web jobs) +./local-test # Full CI parity: Python + web + .NET (Data, Bff, FileService) ./local-test python # Backend: three 100% coverage gates + browser pytest + CLI smoke ./local-test browser # JS crawl integration tests (skips if Chromium unavailable) -./local-test web # Frontend: typecheck, lint, vitest +./local-test web # Frontend: build, typecheck, lint, vitest +./local-test dotnet # dotnet test Data + Bff + FileService + BFF OpenAPI drift gate ./local-test quick # Fast loop; requires DB already running (no coverage gate) ./local-test all --no-cov # Full run without pytest coverage gate ``` -CI also runs a **Docker** job (image build, browser pytest in container, compose smoke). See [.github/workflows/ci.yml](.github/workflows/ci.yml). +CI runs separate jobs for **Python** (coverage gates), **web**, **Data**, **Bff**, **FileService**, and **Docker** (image build, browser pytest in container, compose smoke). See [.github/workflows/ci.yml](.github/workflows/ci.yml). ## Configuration @@ -278,7 +311,7 @@ Ask questions about audit data at [http://localhost:3000/chat](http://localhost: | **Groq** | API key in AI settings or `GROQ_API_KEY`; official Groq Python SDK; native tool calling with streaming. Default model `openai/gpt-oss-120b`. | -The agent uses the same **342 read-only audit tools** as the MCP server ([docs/MCP.md](docs/MCP.md)), with **dynamic routing** (~45 tools per turn). Responses stream over SSE (`POST /api/chat`). Sessions persist per property (`chat_sessions` / `chat_messages`). +The agent uses the same **340 read-only audit tools** as the MCP server ([docs/MCP.md](docs/MCP.md)), with **dynamic routing** (~45 tools per turn). Responses stream over SSE (`POST /api/chat`). Sessions persist per property (`chat_sessions` / `chat_messages`). **Read-only SQL chat tool (opt-in):** Set `CHAT_SQL_TOOL_ENABLED=true` to expose `get_sql_schema` and `run_sql_query` to the LLM. The agent can then answer arbitrary data questions by generating and executing a single read-only SELECT. Queries are validated by a four-layer guard (regex pre-filter → `sqlglot` AST + table allowlist → `BEGIN TRANSACTION READ ONLY` → optional least-privilege DB role); DELETE/UPDATE/INSERT/DDL and non-allowlisted tables are always blocked. In multi-property deployments, scope-binding CTEs are automatically injected to enforce tenant isolation. See [docs/OPS.md](docs/OPS.md#read-only-sql-chat-tool) for setup including the recommended `audit_readonly` Postgres role and optional RLS configuration. diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index e1ccf357..a7e0ae71 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,4 +1,5 @@ -# Production-style layout: postgres + web + worker (same image, different command). +# Production-style layout: postgres + fastapi + worker + bff + web + files (+ optional mcp). +# The browser talks only to `bff`; FastAPI and FileService are network-internal. services: postgres: image: postgres:16-alpine @@ -14,26 +15,19 @@ services: timeout: 3s retries: 5 - web: + fastapi: build: context: . dockerfile: Dockerfile + image: website-profiling:latest depends_on: postgres: condition: service_healthy - ports: - - '${WEB_PORT:-3000}:3000' - - '${FASTAPI_PORT:-8001}:8001' environment: + WP_ROLE: fastapi WEBSITE_PROFILING_ROOT: /app DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-website_profiling} DATA_DIR: /data - AUTH_SECRET: ${AUTH_SECRET:?set AUTH_SECRET} - AUTH_PASSWORD: ${AUTH_PASSWORD:-} - NODE_ENV: production - FASTAPI_URL: http://127.0.0.1:8001 - FASTAPI_ALLOWED_ORIGINS: ${FASTAPI_ALLOWED_ORIGINS:-http://localhost:3000} - FILE_SERVICE_URL: http://files:8080 PYTHON: /opt/venv/bin/python CHROME_PATH: /usr/bin/chromium LIGHTHOUSE_PATH: /usr/local/bin/lighthouse @@ -41,50 +35,82 @@ services: volumes: - profiling-data:/data healthcheck: - test: ['CMD', 'node', '-e', "require('http').get('http://127.0.0.1:3000/api/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"] + test: ['CMD', 'node', '-e', "require('http').get('http://127.0.0.1:8001/api/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"] interval: 30s timeout: 5s retries: 3 start_period: 30s worker: - build: - context: . - dockerfile: Dockerfile + image: website-profiling:latest depends_on: + fastapi: + condition: service_started postgres: condition: service_healthy - command: ['/opt/venv/bin/python', '-m', 'website_profiling.worker'] environment: + WP_ROLE: worker WEBSITE_PROFILING_ROOT: /app DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-website_profiling} DATA_DIR: /data volumes: - profiling-data:/data - profiles: - - worker files: build: context: ./services/FileService + environment: + REPORT_API_URL: http://fastapi:8001 + depends_on: + fastapi: + condition: service_started + + bff: + build: + context: ./services/Bff + depends_on: + fastapi: + condition: service_started + files: + condition: service_started ports: - - '${FILE_SERVICE_PORT:-8080}:8080' + - '${BFF_PORT:-8090}:8090' environment: - REPORT_API_URL: http://web:8001 + FASTAPI_URL: http://fastapi:8001 + FILE_SERVICE_URL: http://files:8080 + AUTH_SECRET: ${AUTH_SECRET:?set AUTH_SECRET} + AUTH_PASSWORD: ${AUTH_PASSWORD:-} + BFF_ALLOWED_ORIGINS: ${BFF_ALLOWED_ORIGINS:?set BFF_ALLOWED_ORIGINS (the public UI origin)} + # Cross-site cookie (frontend + BFF on a shared parent domain over HTTPS): + BFF_COOKIE_SAMESITE: ${BFF_COOKIE_SAMESITE:-None} + BFF_COOKIE_SECURE: ${BFF_COOKIE_SECURE:-true} + BFF_COOKIE_DOMAIN: ${BFF_COOKIE_DOMAIN:-} + + web: + build: + context: ./web + args: + VITE_BFF_BASE_URL: ${BFF_PUBLIC_URL:?set BFF_PUBLIC_URL (browser-facing BFF origin)} depends_on: - web: + bff: condition: service_started + ports: + - '${WEB_PORT:-3000}:80' + healthcheck: + test: ['CMD', 'wget', '-qO-', 'http://127.0.0.1/home'] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s mcp: - build: - context: . - dockerfile: Dockerfile + image: website-profiling:latest depends_on: postgres: condition: service_healthy files: condition: service_started - command: ['python', '-m', 'website_profiling.mcp.http'] + command: ['/opt/venv/bin/python', '-m', 'website_profiling.mcp.http'] environment: WEBSITE_PROFILING_ROOT: /app DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-website_profiling} @@ -96,6 +122,8 @@ services: WP_MCP_ALLOWED_ORIGINS: ${WP_MCP_ALLOWED_ORIGINS:-} WP_MCP_DOMAIN: ${WP_MCP_DOMAIN:-core} WP_PROPERTY_ID: ${WP_PROPERTY_ID:-} + profiles: + - mcp ports: - '${MCP_PORT:-8000}:8000' diff --git a/docker-compose.pull.yml b/docker-compose.pull.yml index 67d072ca..40563899 100644 --- a/docker-compose.pull.yml +++ b/docker-compose.pull.yml @@ -1,7 +1,9 @@ -# Run a pre-built/pulled image with Postgres (no local docker build). +# Run pre-built backend image + Vite web image with Postgres (no monolith UI). # Usage: -# export WEB_IMAGE=your-registry/website-profiling:tag -# docker compose -f docker-compose.pull.yml up -d +# docker build -t website-profiling:ci . +# docker build -t website-profiling-web:ci ./web --build-arg VITE_BFF_BASE_URL=http://localhost:8090 +# BACKEND_IMAGE=website-profiling:ci WEB_IMAGE=website-profiling-web:ci \ +# docker compose -f docker-compose.pull.yml up -d services: postgres: image: postgres:16-alpine @@ -17,28 +19,38 @@ services: timeout: 3s retries: 5 - web: - image: ${WEB_IMAGE:-website-profiling:latest} + fastapi: + image: ${BACKEND_IMAGE:-website-profiling:latest} depends_on: postgres: condition: service_healthy - ports: - - "3000:3000" environment: + WP_ROLE: fastapi WEBSITE_PROFILING_ROOT: /app DATABASE_URL: postgres://profiling:profiling@postgres:5432/website_profiling DATA_DIR: /data PYTHON: /opt/venv/bin/python - NODE_ENV: production CHROME_PATH: /usr/bin/chromium LIGHTHOUSE_PATH: /usr/local/bin/lighthouse LIGHTHOUSE_CHROME_FLAGS: --headless --no-sandbox --disable-dev-shm-usage --disable-gpu - FASTAPI_URL: http://127.0.0.1:8001 - FILE_SERVICE_URL: http://files:8080 volumes: - profiling-data:/data healthcheck: - test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:3000/home', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"] + test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:8001/api/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + + web: + image: ${WEB_IMAGE:-website-profiling-web:latest} + depends_on: + fastapi: + condition: service_healthy + ports: + - "3000:80" + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1/home"] interval: 30s timeout: 5s retries: 3 @@ -47,12 +59,10 @@ services: files: build: context: ./services/FileService - ports: - - "8080:8080" environment: - REPORT_API_URL: http://web:8001 + REPORT_API_URL: http://fastapi:8001 depends_on: - web: + fastapi: condition: service_started volumes: diff --git a/docker-compose.yml b/docker-compose.yml index 02427c6c..07d6bba5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,9 @@ services: timeout: 3s retries: 5 - web: + # FastAPI backend — now its own service (was bundled into `web`). Network-internal: + # only the BFF talks to it, so no host port is published. + fastapi: build: context: . dockerfile: Dockerfile @@ -21,61 +23,112 @@ services: depends_on: postgres: condition: service_healthy - ports: - - "3000:3000" - - "8001:8001" environment: + WP_ROLE: fastapi + WEBSITE_PROFILING_ROOT: /app + DATABASE_URL: postgres://profiling:profiling@postgres:5432/website_profiling + DATA_DIR: /data + PYTHON: /opt/venv/bin/python + CHROME_PATH: /usr/bin/chromium + LIGHTHOUSE_PATH: /usr/local/bin/lighthouse + LIGHTHOUSE_CHROME_FLAGS: --headless --no-sandbox --disable-dev-shm-usage --disable-gpu + FASTAPI_ALLOWED_ORIGINS: "http://localhost:8090" + volumes: + - profiling-data:/data + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:8001/api/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s + + worker: + image: website-profiling:latest + depends_on: + fastapi: + condition: service_started + postgres: + condition: service_healthy + environment: + WP_ROLE: worker WEBSITE_PROFILING_ROOT: /app DATABASE_URL: postgres://profiling:profiling@postgres:5432/website_profiling DATA_DIR: /data PYTHON: /opt/venv/bin/python - NODE_ENV: production CHROME_PATH: /usr/bin/chromium LIGHTHOUSE_PATH: /usr/local/bin/lighthouse LIGHTHOUSE_CHROME_FLAGS: --headless --no-sandbox --disable-dev-shm-usage --disable-gpu - FASTAPI_URL: http://127.0.0.1:8001 - FASTAPI_ALLOWED_ORIGINS: "http://localhost:3000" - FILE_SERVICE_URL: http://files:8080 volumes: - profiling-data:/data + + # .NET read microservice — reads Postgres directly, incrementally replacing FastAPI reads. + # Internal: only the BFF reaches it (no published host port). + data: + build: + context: ./services/Data + environment: + DATABASE_URL: postgres://profiling:profiling@postgres:5432/website_profiling + ASPNETCORE_URLS: http://+:8091 + depends_on: + postgres: + condition: service_healthy healthcheck: - test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:3000/home', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"] + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8091/health"] interval: 30s timeout: 5s retries: 3 start_period: 15s - files: + # .NET BFF — the single browser-facing API surface (owns auth + CORS). + bff: build: - context: ./services/FileService + context: ./services/Bff ports: - - "8080:8080" + - "8090:8090" environment: - REPORT_API_URL: http://web:8001 + FASTAPI_URL: http://fastapi:8001 + FILE_SERVICE_URL: http://files:8080 + BFF_ALLOWED_ORIGINS: "http://localhost:3000" + DATA_SERVICE_URL: http://data:8091 + # Comma-separated /api path prefixes served by the Data service (reads + issue/portfolio mutations). + # Rollback: remove a prefix here and restart bff. + DATA_ROUTES: "/api/report/meta,/api/report/payload,/api/report/history,/api/report/crawl-payload,/api/report/mobile-delta,/api/report/portfolio,/api/portfolio,/api/issues/status,/api/filters" + # AUTH_SECRET: set to enable wp_session auth (must match the web service) + depends_on: + fastapi: + condition: service_started + files: + condition: service_started + data: + condition: service_started + + # Vite SPA (nginx). The browser calls the BFF (:8090) for all /api/*. + web: + build: + context: ./web + args: + VITE_BFF_BASE_URL: http://localhost:8090 depends_on: - web: + bff: condition: service_started + ports: + - "3000:80" + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1/home"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 15s - # Optional remote MCP (Streamable HTTP). Uncomment and set WP_MCP_TOKEN / WP_MCP_ALLOWED_HOSTS. - # mcp: - # build: - # context: . - # dockerfile: Dockerfile - # image: website-profiling:latest - # depends_on: - # postgres: - # condition: service_healthy - # command: ['python', '-m', 'website_profiling.mcp.http'] - # environment: - # WEBSITE_PROFILING_ROOT: /app - # DATABASE_URL: postgres://profiling:profiling@postgres:5432/website_profiling - # WP_MCP_HTTP_HOST: 0.0.0.0 - # WP_MCP_HTTP_PORT: 8000 - # WP_MCP_TOKEN: ${WP_MCP_TOKEN:-dev-mcp-token} - # WP_MCP_ALLOWED_HOSTS: localhost,127.0.0.1 - # WP_MCP_DOMAIN: core - # ports: - # - "8000:8000" + # File export service (PDF/Excel). Internal: only the BFF reaches it. + files: + build: + context: ./services/FileService + environment: + REPORT_API_URL: http://fastapi:8001 + depends_on: + fastapi: + condition: service_started volumes: pg-data: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index d5ec568b..6b93de80 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -2,13 +2,20 @@ set -e cd /app -if [ -z "${DATABASE_URL:-}" ] || [ -z "$(printf '%s' "$DATABASE_URL" | tr -d '[:space:]')" ]; then - echo "ERROR: DATABASE_URL is required." >&2 - echo " Use docker compose (see README) or pass -e DATABASE_URL=postgres://user:pass@host:5432/db" >&2 - exit 1 -fi +# Role dispatch. Default "all" runs worker + FastAPI in one container (legacy). +# Split topology: WP_ROLE=fastapi | worker +ROLE="${WP_ROLE:-all}" + +require_database_url() { + if [ -z "${DATABASE_URL:-}" ] || [ -z "$(printf '%s' "$DATABASE_URL" | tr -d '[:space:]')" ]; then + echo "ERROR: DATABASE_URL is required." >&2 + echo " Use docker compose (see README) or pass -e DATABASE_URL=postgres://user:pass@host:5432/db" >&2 + exit 1 + fi +} -/opt/venv/bin/python <<'PY' +wait_for_db() { + /opt/venv/bin/python <<'PY' import os import sys import time @@ -61,50 +68,59 @@ print( print(f" Last error: {last_error}", file=sys.stderr) sys.exit(1) PY - -/opt/venv/bin/alembic upgrade head - -WORKER_PID="" -UVICORN_PID="" -NPM_PID="" - -cleanup() { - [ -n "$WORKER_PID" ] && kill "$WORKER_PID" 2>/dev/null || true - [ -n "$UVICORN_PID" ] && kill "$UVICORN_PID" 2>/dev/null || true - [ -n "$NPM_PID" ] && kill "$NPM_PID" 2>/dev/null || true } -trap cleanup TERM INT -/opt/venv/bin/python -m website_profiling.worker & -WORKER_PID=$! +migrate() { + /opt/venv/bin/alembic upgrade head +} -/opt/venv/bin/uvicorn website_profiling.api.main:app \ - --host 0.0.0.0 --port 8001 --workers 1 & -UVICORN_PID=$! +start_uvicorn_foreground() { + exec /opt/venv/bin/uvicorn website_profiling.api.main:app \ + --host 0.0.0.0 --port 8001 --workers 1 +} -# Wait for FastAPI to be ready before starting Next.js (max ~15s) -i=0 -while [ "$i" -lt 30 ]; do - if node -e "require('http').get('http://127.0.0.1:8001/api/health',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))" 2>/dev/null; then - echo "FastAPI ready (attempt $((i + 1))/30)" >&2 - break - fi - sleep 0.5 - i=$((i + 1)) -done -if [ "$i" -eq 30 ]; then - echo "WARNING: FastAPI did not respond to /api/health after 15s — continuing anyway" >&2 -fi - -cd /app/web -npm run start -- -H 0.0.0.0 -p 3000 & -NPM_PID=$! - -# Monitor critical processes — exit the container if either npm or uvicorn dies. -# A dead worker does not break the UI so it is intentionally excluded. -while kill -0 "$NPM_PID" 2>/dev/null && kill -0 "$UVICORN_PID" 2>/dev/null; do - sleep 5 -done -echo "Critical process (npm or uvicorn) exited — shutting down container" >&2 -cleanup -exit 1 +case "$ROLE" in + fastapi) + require_database_url + wait_for_db + migrate + start_uvicorn_foreground + ;; + worker) + require_database_url + wait_for_db + exec /opt/venv/bin/python -m website_profiling.worker + ;; + all) + require_database_url + wait_for_db + migrate + + WORKER_PID="" + UVICORN_PID="" + + cleanup() { + [ -n "$WORKER_PID" ] && kill "$WORKER_PID" 2>/dev/null || true + [ -n "$UVICORN_PID" ] && kill "$UVICORN_PID" 2>/dev/null || true + } + trap cleanup TERM INT + + /opt/venv/bin/python -m website_profiling.worker & + WORKER_PID=$! + + /opt/venv/bin/uvicorn website_profiling.api.main:app \ + --host 0.0.0.0 --port 8001 --workers 1 & + UVICORN_PID=$! + + while kill -0 "$UVICORN_PID" 2>/dev/null; do + sleep 5 + done + echo "FastAPI exited — shutting down container" >&2 + cleanup + exit 1 + ;; + *) + echo "ERROR: unknown WP_ROLE '$ROLE' (expected: all | fastapi | worker)" >&2 + exit 1 + ;; +esac diff --git a/docs/README.md b/docs/README.md index 132a6705..e8503538 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,6 +16,7 @@ This directory contains product, integration, and operations documentation for * | [MCP.md](MCP.md) | Integrators | Model Context Protocol server configuration and tool reference | | [OPS.md](OPS.md) | Operators | Scheduled audits, alerts, migrations, production notes | | [services/FileService/README.md](../services/FileService/README.md) | Developers / operators | PDF and Excel workbook export service | +| `services/Data/` | Developers | .NET read service — report payloads, portfolio, issue status, saved filters | --- diff --git a/docs/assets/readme-banner.png b/docs/assets/readme-banner.png index 6c77d150..cfaadcab 100644 Binary files a/docs/assets/readme-banner.png and b/docs/assets/readme-banner.png differ diff --git a/docs/assets/seo-feedback-loop.png b/docs/assets/seo-feedback-loop.png index f243a519..51efaaaa 100644 Binary files a/docs/assets/seo-feedback-loop.png and b/docs/assets/seo-feedback-loop.png differ diff --git a/scripts/local-prod.sh b/scripts/local-prod.sh index 37892ad0..eceb7ef3 100755 --- a/scripts/local-prod.sh +++ b/scripts/local-prod.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash -# Local prod: same Postgres as ./local-run, Next.js build + start (NODE_ENV=production). +# Local prod: same Postgres as ./local-run, Vite build + preview (NODE_ENV=production). # Usage: ./local-prod [command] -# (default) start — DB, migrations, npm run build, npm run start +# (default) start — DB, migrations, npm run build, npm run preview # build — npm run build only # help — show commands set -euo pipefail @@ -26,8 +26,73 @@ WEB="$ROOT/web" LOCAL_RUN="$ROOT/scripts/local-run.sh" log() { printf '\033[1;36m→\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m!\033[0m %s\n' "$*" >&2; } die() { printf '\033[1;31m✗\033[0m %s\n' "$*" >&2; exit 1; } +kill_process_tree() { + local pid="$1" + local sig="${2:-TERM}" + local child + [[ -z "$pid" ]] && return 0 + for child in $(pgrep -P "$pid" 2>/dev/null || true); do + kill_process_tree "$child" "$sig" + done + kill "-$sig" "$pid" 2>/dev/null || true +} + +wait_for_pid() { + local pid="$1" + local timeout="${2:-10}" + local i + [[ -z "$pid" ]] && return 0 + for ((i = 0; i < timeout * 2; i++)); do + kill -0 "$pid" 2>/dev/null || return 0 + sleep 0.5 + done + return 1 +} + +stop_service() { + local name="$1" + local pid="$2" + [[ -z "$pid" ]] && return 0 + if ! kill -0 "$pid" 2>/dev/null; then + wait "$pid" 2>/dev/null || true + log "$name already stopped." + return 0 + fi + log "Stopping $name (PID $pid)..." + kill_process_tree "$pid" TERM + if ! wait_for_pid "$pid" 10; then + warn "$name did not exit in time — sending SIGKILL" + kill_process_tree "$pid" KILL + wait_for_pid "$pid" 2 || true + fi + wait "$pid" 2>/dev/null || true + log "$name stopped." +} + +disown_bg() { + local pid="$1" + [[ -z "$pid" ]] && return 0 + disown "$pid" 2>/dev/null || true +} + +stop_postgres() { + if ! command -v docker >/dev/null 2>&1; then + return 0 + fi + if ! docker info >/dev/null 2>&1; then + warn "Docker unavailable — skipping Postgres stop" + return 0 + fi + if docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$PG_CONTAINER"; then + log "Stopping $PG_CONTAINER" + docker stop "$PG_CONTAINER" >/dev/null 2>&1 || warn "Could not stop $PG_CONTAINER" + log "Postgres stopped." + fi +} + need_cmd() { command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1" } @@ -42,7 +107,7 @@ cmd_web_deps() { cmd_build() { cmd_web_deps - log "Building Next.js (production)" + log "Building Vite SPA (production)" (cd "$WEB" && npm run build) } @@ -63,7 +128,7 @@ cmd_start() { cmd_web_deps log "Skipping build (--skip-build)" fi - log "Starting Next.js production server (Ctrl+C to stop)" + log "Starting Vite preview server (Ctrl+C stops all services including Postgres)" log "DATABASE_URL=$DATABASE_URL" log "DATA_DIR=$DATA_DIR" log "PYTHON=$PYTHON" @@ -74,37 +139,58 @@ cmd_start() { WORKER_PID="" UVICORN_PID="" NPM_PID="" + _CLEANUP_DONE=0 + set +m cleanup_prod() { - [ -n "$WORKER_PID" ] && kill "$WORKER_PID" 2>/dev/null || true - [ -n "$UVICORN_PID" ] && kill "$UVICORN_PID" 2>/dev/null || true - [ -n "$NPM_PID" ] && kill "$NPM_PID" 2>/dev/null || true + if [[ "$_CLEANUP_DONE" -eq 1 ]]; then + return 0 + fi + _CLEANUP_DONE=1 + trap - INT TERM EXIT + set +e + + log "Shutting down local prod stack..." + stop_service "Vite preview" "$NPM_PID" + NPM_PID="" + stop_service "FastAPI" "$UVICORN_PID" + UVICORN_PID="" + stop_service "pipeline worker" "$WORKER_PID" + WORKER_PID="" + stop_postgres + log "All services stopped." + exit 0 } - trap cleanup_prod INT TERM EXIT + trap cleanup_prod EXIT INT TERM log "Starting pipeline worker" "$ROOT/.venv/bin/python" -m website_profiling.worker & WORKER_PID=$! + disown_bg "$WORKER_PID" log "Starting FastAPI on port 8001" export FASTAPI_URL="http://127.0.0.1:8001" "$ROOT/.venv/bin/uvicorn" website_profiling.api.main:app \ --host 0.0.0.0 --port 8001 --workers 1 & UVICORN_PID=$! + disown_bg "$UVICORN_PID" cd "$WEB" - npm run start -- -H 0.0.0.0 -p 3000 & + npm run preview -- --host 0.0.0.0 --port 3000 & NPM_PID=$! - wait $NPM_PID + disown_bg "$NPM_PID" + set +e + wait "$NPM_PID" + exit 0 } cmd_help() { cat </dev/null || true); do + kill_process_tree "$child" "$sig" + done + kill "-$sig" "$pid" 2>/dev/null || true +} + +wait_for_pid() { + local pid="$1" + local timeout="${2:-10}" + local i + [[ -z "$pid" ]] && return 0 + for ((i = 0; i < timeout * 2; i++)); do + kill -0 "$pid" 2>/dev/null || return 0 + sleep 0.5 + done + return 1 +} + +stop_service() { + local name="$1" + local pid="$2" + local port="${3:-}" + [[ -z "$pid" ]] && return 0 + if ! kill -0 "$pid" 2>/dev/null; then + wait "$pid" 2>/dev/null || true + log "$name already stopped." + [[ -n "$port" ]] && free_port "$port" + return 0 + fi + log "Stopping $name (PID $pid)..." + kill_process_tree "$pid" TERM + if ! wait_for_pid "$pid" 10; then + warn "$name did not exit in time — sending SIGKILL" + kill_process_tree "$pid" KILL + wait_for_pid "$pid" 2 || true + fi + wait "$pid" 2>/dev/null || true + log "$name stopped." + [[ -n "$port" ]] && free_port "$port" +} + +# Detach background jobs so bash does not print "Terminated" after cleanup. +disown_bg() { + local pid="$1" + [[ -z "$pid" ]] && return 0 + disown "$pid" 2>/dev/null || true +} + need_cmd() { command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1" } @@ -153,13 +208,36 @@ cmd_start() { WORKER_PID="" UVICORN_PID="" FILE_SERVICE_PID="" + DATA_PID="" + BFF_PID="" + _CLEANUP_DONE=0 + set +m cleanup_local() { - [ -n "$WORKER_PID" ] && kill "$WORKER_PID" 2>/dev/null || true - [ -n "$UVICORN_PID" ] && kill "$UVICORN_PID" 2>/dev/null || true - [ -n "$FILE_SERVICE_PID" ] && kill "$FILE_SERVICE_PID" 2>/dev/null || true + if [[ "$_CLEANUP_DONE" -eq 1 ]]; then + return 0 + fi + _CLEANUP_DONE=1 + trap - INT TERM EXIT + set +e + + log "Shutting down local dev stack..." + # Reverse startup order; Vite (foreground) is already exiting from Ctrl+C. + stop_service "BFF" "$BFF_PID" 8090 + BFF_PID="" + stop_service "Data" "$DATA_PID" 8091 + DATA_PID="" + stop_service "FastAPI" "$UVICORN_PID" 8001 + UVICORN_PID="" + stop_service "pipeline worker" "$WORKER_PID" + WORKER_PID="" + stop_service "FileService" "$FILE_SERVICE_PID" 8080 + FILE_SERVICE_PID="" + stop_postgres + log "All services stopped." + exit 0 } - trap cleanup_local INT TERM EXIT + trap cleanup_local EXIT INT TERM if command -v dotnet >/dev/null 2>&1; then free_port 8080 @@ -170,65 +248,125 @@ cmd_start() { ASPNETCORE_ENVIRONMENT=Development \ dotnet run --project src/FileService.Api --no-launch-profile) & FILE_SERVICE_PID=$! + disown_bg "$FILE_SERVICE_PID" + + free_port 8091 + log "Starting Data service on port 8091" + (cd "$ROOT/services/Data" && \ + DATABASE_URL="$DATABASE_URL" \ + ASPNETCORE_URLS="http://127.0.0.1:8091" \ + ASPNETCORE_ENVIRONMENT=Development \ + dotnet run --project src/Data.Api --no-launch-profile) & + DATA_PID=$! + disown_bg "$DATA_PID" else warn "dotnet not found — PDF export requires FileService (see services/FileService/README.md)" + warn "dotnet not found — Data service unavailable on port 8091" fi log "Starting pipeline worker" "$VENV/bin/python" -m website_profiling.worker & WORKER_PID=$! + disown_bg "$WORKER_PID" free_port 8001 log "Starting FastAPI on port 8001" export FASTAPI_URL="http://127.0.0.1:8001" - export FASTAPI_ALLOWED_ORIGINS="http://localhost:3000" + export FASTAPI_ALLOWED_ORIGINS="http://localhost:8090" "$VENV/bin/uvicorn" website_profiling.api.main:app \ --host 0.0.0.0 --port 8001 --workers 1 & UVICORN_PID=$! + disown_bg "$UVICORN_PID" + + if command -v dotnet >/dev/null 2>&1; then + free_port 8090 + log "Starting BFF on port 8090" + (cd "$ROOT/services/Bff" && \ + FASTAPI_URL="http://127.0.0.1:8001" \ + FILE_SERVICE_URL="${FILE_SERVICE_URL:-http://127.0.0.1:8080}" \ + DATA_SERVICE_URL="http://127.0.0.1:8091" \ + DATA_ROUTES="${DATA_ROUTES:-/api/report/meta,/api/report/payload,/api/report/history,/api/report/crawl-payload,/api/report/mobile-delta,/api/report/portfolio,/api/portfolio,/api/issues/status,/api/filters}" \ + BFF_ALLOWED_ORIGINS="http://localhost:3000" \ + ASPNETCORE_URLS="http://127.0.0.1:8090" \ + ASPNETCORE_ENVIRONMENT=Development \ + dotnet run --project src/Bff.Api --no-launch-profile) & + BFF_PID=$! + disown_bg "$BFF_PID" + else + warn "dotnet not found — browser API calls need the BFF (see services/Bff/)" + fi - log "Starting Next.js dev server (Ctrl+C to stop)" + log "Starting Vite dev server (Ctrl+C stops all services including Postgres)" log "DATABASE_URL=$DATABASE_URL" log "DATA_DIR=$DATA_DIR" log "PYTHON=$PYTHON" + log "VITE_BFF_BASE_URL=${VITE_BFF_BASE_URL:-http://localhost:8090}" log "FILE_SERVICE_URL=${FILE_SERVICE_URL:-http://127.0.0.1:8080}" + log "DATA_ROUTES=${DATA_ROUTES:-/api/report/meta,...}" export FILE_SERVICE_URL="${FILE_SERVICE_URL:-http://127.0.0.1:8080}" + export VITE_BFF_BASE_URL="${VITE_BFF_BASE_URL:-http://localhost:8090}" cd "$WEB" - # Do not exec — keep this shell alive so the trap kills FileService/worker/uvicorn on Ctrl+C. + set +e npm run dev + exit 0 } cmd_stop() { - ensure_docker + need_cmd docker + if ! docker info >/dev/null 2>&1; then + die "Docker is not running. Start Docker Desktop, then retry." + fi if docker ps --format '{{.Names}}' | grep -qx "$PG_CONTAINER"; then - log "Stopping $PG_CONTAINER" - docker stop "$PG_CONTAINER" >/dev/null + stop_postgres else warn "Container $PG_CONTAINER is not running" fi } +stop_postgres() { + if ! command -v docker >/dev/null 2>&1; then + return 0 + fi + if ! docker info >/dev/null 2>&1; then + warn "Docker unavailable — skipping Postgres stop" + return 0 + fi + if docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$PG_CONTAINER"; then + log "Stopping $PG_CONTAINER" + docker stop "$PG_CONTAINER" >/dev/null 2>&1 || warn "Could not stop $PG_CONTAINER" + log "Postgres stopped." + fi +} + +cmd_test() { + shift + exec "$ROOT/scripts/local-test.sh" all "$@" +} + cmd_help() { cat </data) + DATA_ROUTES (default: report reads, portfolio, issues status, saved filters) WP_PG_CONTAINER, WP_PG_PORT, WP_PG_PASSWORD, WP_PG_DB After start, open: http://localhost:3000/home Run audits via sidebar "Run audit" (bottom-right FAB). -Production Next.js (same Postgres, no hot reload): ./local-prod start +Production build (same Postgres, no hot reload): ./local-prod start -Run CI-style tests: ./local-test (see ./local-test help). JS crawl integration: ./local-test browser. +Run CI-style tests: ./local-test or ./local-run test (see ./local-test help). EOF } @@ -239,6 +377,7 @@ main() { setup) cmd_setup ;; db) cmd_db ;; migrate) cmd_migrate ;; + test) cmd_test "$@" ;; stop) cmd_stop ;; help|-h|--help) cmd_help ;; *) diff --git a/scripts/local-test.sh b/scripts/local-test.sh index b16595d9..dd2c1d7c 100755 --- a/scripts/local-test.sh +++ b/scripts/local-test.sh @@ -1,12 +1,13 @@ #!/usr/bin/env bash # Local test runner — mirrors .github/workflows/ci.yml on your machine. # Usage: ./local-test [command] [--no-cov] -# (default) all — Postgres + migrations + Python + web checks +# (default) all — Postgres + migrations + Python + web + .NET (Data, Bff, FileService) # python — DB + pytest + CLI smoke only -# web — typecheck, lint, vitest (no Postgres) -# quick — pytest --no-cov + web (DB must already be running) +# web — build, typecheck, lint, vitest (no Postgres) +# dotnet — dotnet test Data + Bff + FileService + Bff OpenAPI drift gate +# quick — pytest --no-cov + web + dotnet (DB must already be running) # help — show commands -set -euo pipefail +set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" @@ -27,41 +28,108 @@ VENV="$ROOT/.venv" WEB="$ROOT/web" PYTEST_NO_COV=0 +STEP_PASS=() +STEP_FAIL=() # entries: "name|detail" +STEP_SKIP=() # entries: "name|reason" + log() { printf '\033[1;36m→\033[0m %s\n' "$*"; } ok() { printf '\033[1;32m✓\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m!\033[0m %s\n' "$*" >&2; } -die() { printf '\033[1;31m✗\033[0m %s\n' "$*" >&2; exit 1; } +fail_msg() { printf '\033[1;31m✗\033[0m %s\n' "$*" >&2; } +die() { fail_msg "$*"; exit 1; } -need_cmd() { - command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1" +reset_steps() { + STEP_PASS=() + STEP_FAIL=() + STEP_SKIP=() } -ensure_docker() { - need_cmd docker - if ! docker info >/dev/null 2>&1; then - die "Docker is not running. Start Docker Desktop, then retry (or: ./local-test quick with DATABASE_URL set)." +run_step() { + local name="$1" + shift + log "$name" + local ec=0 + "$@" || ec=$? + if [[ "$ec" -eq 0 ]]; then + STEP_PASS+=("$name") + else + STEP_FAIL+=("$name|exit code $ec") fi } -wait_for_postgres() { - local i - for i in $(seq 1 30); do - if docker exec "$PG_CONTAINER" pg_isready -U "$PG_USER" -d "$PG_DB" >/dev/null 2>&1; then - return 0 - fi - sleep 1 - done - die "Postgres did not become ready in time (container: $PG_CONTAINER)" +skip_step() { + local name="$1" + local reason="${2:-skipped}" + warn "$name — $reason" + STEP_SKIP+=("$name|$reason") +} + +print_summary() { + local total_pass=${#STEP_PASS[@]} + local total_fail=${#STEP_FAIL[@]} + local total_skip=${#STEP_SKIP[@]} + local entry name detail + + printf '\n' + printf '\033[1m═══════════════════════════════════════════════════════════════\033[0m\n' + printf '\033[1m Test summary\033[0m\n' + printf '\033[1m═══════════════════════════════════════════════════════════════\033[0m\n' + + if [[ "$total_pass" -gt 0 ]]; then + printf '\n\033[1;32mPASSED (%d)\033[0m\n' "$total_pass" + for name in "${STEP_PASS[@]}"; do + printf ' \033[1;32m✓\033[0m %s\n' "$name" + done + fi + + if [[ "$total_fail" -gt 0 ]]; then + printf '\n\033[1;31mFAILED (%d)\033[0m\n' "$total_fail" + for entry in "${STEP_FAIL[@]}"; do + name="${entry%%|*}" + detail="${entry#*|}" + printf ' \033[1;31m✗\033[0m %s (%s)\n' "$name" "$detail" + done + fi + + if [[ "$total_skip" -gt 0 ]]; then + printf '\n\033[1;33mSKIPPED (%d)\033[0m\n' "$total_skip" + for entry in "${STEP_SKIP[@]}"; do + name="${entry%%|*}" + detail="${entry#*|}" + printf ' \033[1;33m-\033[0m %s (%s)\n' "$name" "$detail" + done + fi + + printf '\n\033[1m───────────────────────────────────────────────────────────────\033[0m\n' + if [[ "$total_fail" -eq 0 ]]; then + ok "All steps passed ($total_pass passed, $total_skip skipped)" + else + fail_msg "$total_fail failed, $total_pass passed, $total_skip skipped" + fi + printf '\n' } -cmd_db() { - ensure_docker +finish() { + print_summary + [[ ${#STEP_FAIL[@]} -eq 0 ]] +} + +need_cmd() { + command -v "$1" >/dev/null 2>&1 +} + +start_postgres() { + need_cmd docker || { warn "docker not found"; return 1; } + if ! docker info >/dev/null 2>&1; then + warn "Docker is not running" + return 1 + fi if docker ps -a --format '{{.Names}}' | grep -qx "$PG_CONTAINER"; then if docker ps --format '{{.Names}}' | grep -qx "$PG_CONTAINER"; then log "Postgres already running ($PG_CONTAINER)" else log "Starting existing container $PG_CONTAINER" - docker start "$PG_CONTAINER" >/dev/null + docker start "$PG_CONTAINER" >/dev/null || return 1 fi else log "Creating Postgres container $PG_CONTAINER on port $PG_PORT" @@ -69,51 +137,57 @@ cmd_db() { -e "POSTGRES_PASSWORD=$PG_PASSWORD" \ -e "POSTGRES_DB=$PG_DB" \ -p "${PG_PORT}:5432" \ - "$PG_IMAGE" >/dev/null + "$PG_IMAGE" >/dev/null || return 1 fi - wait_for_postgres - log "DATABASE_URL=$DATABASE_URL" + local i + for i in $(seq 1 30); do + if docker exec "$PG_CONTAINER" pg_isready -U "$PG_USER" -d "$PG_DB" >/dev/null 2>&1; then + log "DATABASE_URL=$DATABASE_URL" + return 0 + fi + sleep 1 + done + warn "Postgres did not become ready in time (container: $PG_CONTAINER)" + return 1 } -cmd_venv() { - need_cmd python3 +ensure_venv() { + need_cmd python3 || { warn "python3 not found"; return 1; } if [[ ! -x "$VENV/bin/python" ]]; then log "Creating Python venv at .venv" - python3 -m venv "$VENV" + python3 -m venv "$VENV" || return 1 fi if [[ ! -x "$VENV/bin/pytest" ]]; then log "Installing Python dependencies" - "$VENV/bin/pip" install -q -r "$ROOT/requirements.txt" + "$VENV/bin/pip" install -q -r "$ROOT/requirements.txt" || return 1 fi + return 0 } -cmd_migrate() { - [[ -x "$VENV/bin/alembic" ]] || cmd_venv - log "Applying database migrations (alembic upgrade head)" +run_migrate() { + [[ -x "$VENV/bin/alembic" ]] || ensure_venv || return 1 "$VENV/bin/alembic" upgrade head } -cmd_web_deps() { - need_cmd npm +ensure_web_deps() { + need_cmd npm || { warn "npm not found"; return 1; } if [[ ! -d "$WEB/node_modules" ]]; then log "Installing web dependencies (npm ci)" - (cd "$WEB" && npm ci) + (cd "$WEB" && npm ci) || return 1 fi + return 0 } run_pytest_core() { if [[ "$PYTEST_NO_COV" -eq 1 ]]; then - log "Pytest (tests/ -q -m not browser --no-cov)" "$VENV/bin/pytest" tests/ -q -m "not browser" --no-cov else - log "Pytest (tests/ -q -m not browser, core 100% coverage gate)" "$VENV/bin/pytest" tests/ -q -m "not browser" fi } run_pytest_reporting() { [[ "$PYTEST_NO_COV" -eq 1 ]] && return 0 - log "Pytest (reporting coverage gate, 100%)" "$VENV/bin/pytest" \ tests/reporting/ \ --cov=website_profiling.reporting \ @@ -126,7 +200,6 @@ run_pytest_reporting() { run_pytest_tools() { [[ "$PYTEST_NO_COV" -eq 1 ]] && return 0 - log "Pytest (tools coverage gate, 100%)" "$VENV/bin/pytest" \ tests/tools/ \ tests/clients/ \ @@ -138,87 +211,207 @@ run_pytest_tools() { -o addopts= } -run_pytest() { - run_pytest_core - run_pytest_reporting - run_pytest_tools -} - run_browser_pytest() { if "$VENV/bin/python" -c "from website_profiling.crawl.fetchers import browser_status; import sys; sys.exit(0 if browser_status().get('ok') else 1)" 2>/dev/null; then - log "Browser pytest (tests/test_crawl_fetchers.py tests/test_crawler_browser_e2e.py -m browser)" "$VENV/bin/pytest" tests/test_crawl_fetchers.py tests/test_crawler_browser_e2e.py -m browser -q --no-cov else - warn "Chromium unavailable — skipping browser integration tests" + return 2 fi } -cmd_python() { - cmd_db - cmd_venv - cmd_migrate - run_pytest - run_browser_pytest - log "CLI smoke (python -m src --help)" +run_cli_smoke() { "$VENV/bin/python" -m src --help >/dev/null - ok "Python checks passed" +} + +run_web_build() { (cd "$WEB" && npm run build); } +run_web_typecheck() { (cd "$WEB" && npm run typecheck); } +run_web_lint() { (cd "$WEB" && npm run lint); } +run_web_test() { (cd "$WEB" && npm test); } + +dotnet_test_sln() { + local service_dir="$1" + local slnx="$2" + (cd "$ROOT/services/${service_dir}" && dotnet test "$slnx") +} + +run_bff_openapi_drift_gate() { + if ! need_cmd dotnet; then + return 0 + fi + if ! need_cmd nswag; then + if dotnet tool list -g 2>/dev/null | grep -q NSwag.ConsoleCore; then + export PATH="$PATH:$HOME/.dotnet/tools" + else + log "Installing NSwag.ConsoleCore (Bff OpenAPI drift gate)" + dotnet tool install -g NSwag.ConsoleCore || return 1 + export PATH="$PATH:$HOME/.dotnet/tools" + fi + fi + if ! need_cmd nswag; then + return 2 + fi + (cd "$ROOT/services/Bff" && nswag run nswag.json) || return 1 + git diff --exit-code services/Bff/src/Bff.Application/Generated/FastApiClient.g.cs +} + +run_step_or_skip_browser() { + local name="Browser pytest (tests/test_crawl_fetchers.py, tests/test_crawler_browser_e2e.py)" + log "$name" + local ec=0 + run_browser_pytest || ec=$? + if [[ "$ec" -eq 0 ]]; then + STEP_PASS+=("$name") + elif [[ "$ec" -eq 2 ]]; then + skip_step "$name" "Chromium unavailable" + else + STEP_FAIL+=("$name|exit code $ec") + fi +} + +run_step_or_skip_openapi() { + local name="Bff OpenAPI drift gate (FastApiClient.g.cs)" + log "$name" + local ec=0 + run_bff_openapi_drift_gate || ec=$? + if [[ "$ec" -eq 0 ]]; then + STEP_PASS+=("$name") + elif [[ "$ec" -eq 2 ]]; then + skip_step "$name" "nswag not on PATH" + else + STEP_FAIL+=("$name|exit code $ec — run services/Bff/generate-client.sh and commit") + fi +} + +steps_postgres() { + run_step "Postgres ($PG_CONTAINER)" start_postgres +} + +steps_venv() { + run_step "Python venv + dependencies" ensure_venv +} + +steps_migrate() { + run_step "Database migrations (alembic upgrade head)" run_migrate +} + +steps_pytest() { + if [[ "$PYTEST_NO_COV" -eq 1 ]]; then + run_step "Pytest core (tests/ — no coverage)" run_pytest_core + skip_step "Pytest reporting coverage gate" "--no-cov" + skip_step "Pytest tools coverage gate" "--no-cov" + else + run_step "Pytest core (tests/ — 100% coverage gate)" run_pytest_core + run_step "Pytest reporting coverage gate (tests/reporting/)" run_pytest_reporting + run_step "Pytest tools coverage gate (tests/tools/, tests/clients/)" run_pytest_tools + fi +} + +steps_browser() { + run_step_or_skip_browser +} + +steps_cli_smoke() { + run_step "CLI smoke (python -m src --help)" run_cli_smoke +} + +steps_web_deps() { + run_step "Web dependencies (npm ci if needed)" ensure_web_deps +} + +steps_web() { + steps_web_deps + run_step "Web build (web/)" run_web_build + run_step "Web typecheck (web/)" run_web_typecheck + run_step "Web lint (web/)" run_web_lint + run_step "Web tests / vitest (web/)" run_web_test +} + +steps_dotnet() { + if ! need_cmd dotnet; then + skip_step ".NET tests (Data, Bff, FileService)" "dotnet not found" + return 0 + fi + run_step "dotnet test Data (services/Data/Data.slnx)" dotnet_test_sln "Data" "Data.slnx" + run_step "dotnet test Bff (services/Bff/Bff.slnx)" dotnet_test_sln "Bff" "Bff.slnx" + run_step_or_skip_openapi + run_step "dotnet test FileService (services/FileService/FileService.slnx)" dotnet_test_sln "FileService" "FileService.slnx" +} + +steps_python() { + steps_postgres + steps_venv + steps_migrate + steps_pytest + steps_browser + steps_cli_smoke +} + +cmd_python() { + reset_steps + steps_python + finish } cmd_browser() { - cmd_venv - run_browser_pytest - ok "Browser pytest finished" + reset_steps + run_step "Python venv + dependencies" ensure_venv + run_step_or_skip_browser + finish } cmd_web() { - cmd_web_deps - log "Web typecheck" - (cd "$WEB" && npm run typecheck) - log "Web lint" - (cd "$WEB" && npm run lint) - log "Web tests (vitest)" - (cd "$WEB" && npm test) - ok "Web checks passed" + reset_steps + steps_web + finish +} + +cmd_dotnet() { + reset_steps + steps_dotnet + finish } cmd_all() { - cmd_python - cmd_web - ok "All local tests passed (CI python + web jobs, including reporting/tools gates)" + reset_steps + steps_python + steps_web + steps_dotnet + finish } cmd_quick() { if [[ -z "${DATABASE_URL:-}" ]]; then die "DATABASE_URL is not set. Export it or run ./local-test all." fi - cmd_venv - cmd_web_deps + reset_steps warn "quick: assuming Postgres is up and migrated (./local-run db && ./local-run migrate)" - run_pytest - log "CLI smoke (python -m src --help)" - "$VENV/bin/python" -m src --help >/dev/null - log "Web typecheck" - (cd "$WEB" && npm run typecheck) - log "Web lint" - (cd "$WEB" && npm run lint) - log "Web tests (vitest)" - (cd "$WEB" && npm test) - ok "Quick test run passed" + PYTEST_NO_COV=1 + steps_venv + steps_pytest + steps_cli_smoke + steps_web + steps_dotnet + finish } cmd_help() { cat < bool: + text = path.read_text() + original = text + + text = LINK_IMPORT.sub("import { Link } from 'react-router-dom';\n", text) + text = DYNAMIC_IMPORT.sub("", text) + + def nav_repl(m: re.Match[str]) -> str: + names = [n.strip() for n in m.group(1).split(",")] + mapping = { + "useRouter": "useNavigate", + "usePathname": None, # useLocation + "useSearchParams": "useSearchParams", + "useParams": "useParams", + "notFound": None, + "redirect": None, + } + out: list[str] = [] + needs_location = "usePathname" in names + for n in names: + if n == "usePathname": + continue + if n == "notFound": + continue + out.append(mapping.get(n, n)) + imports = list(dict.fromkeys(out)) + if needs_location: + imports = ["useLocation", *imports] + return f"import {{ {', '.join(imports)} }} from 'react-router-dom';\n" + + text = NAV_IMPORT.sub(nav_repl, text) + + # Link href -> to + text = re.sub(r" useLocation + if "useLocation" in text and "usePathname" in text: + text = re.sub( + r"\bconst pathname = usePathname\(\)", + "const { pathname } = useLocation()", + text, + ) + + # useSearchParams destructuring + text = re.sub( + r"\bconst searchParams = useSearchParams\(\)", + "const [searchParams] = useSearchParams()", + text, + ) + + # router.push/replace -> navigate + text = re.sub( + r"router\.replace\(([^,)]+),\s*\{\s*scroll:\s*false\s*\}\)", + r"navigate(\1, { replace: true, preventScrollReset: true })", + text, + ) + text = re.sub(r"router\.replace\(", "navigate(", text) + text = re.sub(r"router\.push\(", "navigate(", text) + text = re.sub(r"router\.back\(", "navigate(-1", text) + + # navigate(x) from replace needs { replace: true } when it was router.replace without scroll option + # Fix navigate calls that came from router.replace(single arg) - already handled above except + # we need replace: true for plain router.replace(path) + # Re-run: navigate(q ? ... : pathname) from replace should have replace: true + # Heuristic: lines with navigate( that were from replace - hard to fix automatically. + # Manual fix for ReportShell etc. + + # goToPipeline(router.push -> goToPipeline(navigate + text = text.replace("goToPipeline(router.push", "goToPipeline(navigate") + + # next/dynamic -> lazy + text = re.sub( + r"const (\w+) = dynamic\(\(\) => import\(([^)]+)\),\s*\{[^}]*loading:[^}]*\}\);", + r"const \1 = lazy(() => import(\2));", + text, + ) + text = re.sub( + r"const (\w+) = dynamic\(\(\) => import\(([^)]+)\),\s*\{[^}]*ssr:\s*false,[^}]*\}\);", + r"const \1 = lazy(() => import(\2));", + text, + ) + + # Add lazy import if lazy( used + if "lazy(" in text and "from 'react'" in text: + if re.search(r"import \{[^}]*\blazy\b", text): + pass + elif re.search(r"import \{([^}]+)\} from 'react'", text): + text = re.sub( + r"import \{([^}]+)\} from 'react'", + lambda m: f"import {{ {m.group(1).strip()}, lazy }} from 'react'" + if "lazy" not in m.group(1) + else m.group(0), + text, + count=1, + ) + else: + text = "import { lazy } from 'react';\n" + text + + # process.env.NODE_ENV -> import.meta.env.DEV / PROD + text = text.replace("process.env.NODE_ENV !== 'production'", "import.meta.env.DEV") + text = text.replace("process.env.NODE_ENV === 'production'", "import.meta.env.PROD") + + if text != original: + path.write_text(text) + return True + return False + + +def main() -> None: + changed = 0 + for path in ROOT.rglob("*"): + if path.suffix not in {".ts", ".tsx"}: + continue + if migrate_file(path): + changed += 1 + print(path.relative_to(ROOT.parent)) + print(f"Updated {changed} files") + + +if __name__ == "__main__": + main() diff --git a/services/Bff/.dockerignore b/services/Bff/.dockerignore new file mode 100644 index 00000000..ab41d99e --- /dev/null +++ b/services/Bff/.dockerignore @@ -0,0 +1,4 @@ +**/bin/ +**/obj/ +**/.vs/ +**/TestResults/ diff --git a/services/Bff/Bff.slnx b/services/Bff/Bff.slnx new file mode 100644 index 00000000..90d068a3 --- /dev/null +++ b/services/Bff/Bff.slnx @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/services/Bff/Dockerfile b/services/Bff/Dockerfile new file mode 100644 index 00000000..54e2bbe0 --- /dev/null +++ b/services/Bff/Dockerfile @@ -0,0 +1,16 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY Bff.slnx ./ +COPY src/Bff.Domain/Bff.Domain.csproj src/Bff.Domain/ +COPY src/Bff.Application/Bff.Application.csproj src/Bff.Application/ +COPY src/Bff.Api/Bff.Api.csproj src/Bff.Api/ +RUN dotnet restore src/Bff.Api/Bff.Api.csproj +COPY src/ src/ +RUN dotnet publish src/Bff.Api/Bff.Api.csproj -c Release -o /app/publish --no-restore + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime +WORKDIR /app +ENV ASPNETCORE_URLS=http://+:8090 +EXPOSE 8090 +COPY --from=build /app/publish . +ENTRYPOINT ["dotnet", "Bff.Api.dll"] diff --git a/services/Bff/generate-client.sh b/services/Bff/generate-client.sh new file mode 100755 index 00000000..8937623a --- /dev/null +++ b/services/Bff/generate-client.sh @@ -0,0 +1,10 @@ +#!/bin/sh +# Regenerate the typed FastAPI client (src/Bff.Application/Generated/FastApiClient.g.cs) +# from the committed OpenAPI spec (web/openapi.json). +# +# Prerequisite (one-time): dotnet tool install -g NSwag.ConsoleCore +# The spec itself is produced by: python scripts/generate_openapi.py (run from the repo). +set -e +cd "$(dirname "$0")" +nswag run nswag.json +echo "Generated src/Bff.Application/Generated/FastApiClient.g.cs" diff --git a/services/Bff/nswag.json b/services/Bff/nswag.json new file mode 100644 index 00000000..0cc862c8 --- /dev/null +++ b/services/Bff/nswag.json @@ -0,0 +1,32 @@ +{ + "runtime": "Net100", + "defaultVariables": null, + "documentGenerator": { + "fromDocument": { + "url": "../../web/openapi.json" + } + }, + "codeGenerators": { + "openApiToCSharpClient": { + "clientBaseClass": null, + "className": "FastApiClient", + "operationGenerationMode": "SingleClientFromOperationId", + "generateClientInterfaces": true, + "generateOptionalParameters": true, + "jsonLibrary": "SystemTextJson", + "anyType": "object", + "dictionaryType": "System.Collections.Generic.Dictionary", + "dictionaryInstanceType": "System.Collections.Generic.Dictionary", + "arrayType": "System.Collections.Generic.List", + "arrayInstanceType": "System.Collections.Generic.List", + "namespace": "Bff.Application.Generated", + "requiredPropertiesMustBeDefined": true, + "generateDataAnnotations": false, + "generateExceptionClasses": true, + "exceptionClass": "FastApiClientException", + "useBaseUrl": false, + "generateNullableReferenceTypes": true, + "output": "src/Bff.Application/Generated/FastApiClient.g.cs" + } + } +} diff --git a/services/Bff/src/Bff.Api/Auth/AccessControlMiddleware.cs b/services/Bff/src/Bff.Api/Auth/AccessControlMiddleware.cs new file mode 100644 index 00000000..985719e9 --- /dev/null +++ b/services/Bff/src/Bff.Api/Auth/AccessControlMiddleware.cs @@ -0,0 +1,70 @@ +using System.Security.Claims; +using Bff.Application.Options; +using Bff.Domain; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +namespace Bff.Api.Auth; + +///

+/// Enforces for every request using the principal populated by +/// . Emits ProblemDetails (401/403) on denial. +/// When auth is disabled (no AUTH_SECRET) everything is permitted, matching the TS contract. +/// +public sealed class AccessControlMiddleware(RequestDelegate next, IOptions auth) +{ + private readonly AuthOptions _auth = auth.Value; + + public async Task InvokeAsync(HttpContext context) + { + if (!_auth.Enabled) + { + await next(context); + return; + } + + var requirement = AccessPolicy.Resolve(context.Request.Method, context.Request.Path); + if (requirement == AccessRequirement.Anonymous) + { + await next(context); + return; + } + + var user = context.User; + var authenticated = user.Identity?.IsAuthenticated == true; + if (!authenticated) + { + await WriteProblem(context, StatusCodes.Status401Unauthorized, "Authentication required"); + return; + } + + var role = user.FindFirstValue(ClaimTypes.Role); + var allowed = requirement switch + { + AccessRequirement.Read => true, // any authenticated role, including read-only + AccessRequirement.Mutate => Roles.CanMutate(role), + AccessRequirement.Chat => Roles.CanChat(role), + _ => false, + }; + + if (!allowed) + { + await WriteProblem(context, StatusCodes.Status403Forbidden, "Forbidden"); + return; + } + + await next(context); + } + + private static Task WriteProblem(HttpContext context, int status, string title) + { + context.Response.StatusCode = status; + context.Response.ContentType = "application/problem+json"; + var problem = new ProblemDetails + { + Status = status, + Title = title, + }; + return context.Response.WriteAsJsonAsync(problem, problem.GetType(), options: null, contentType: "application/problem+json"); + } +} diff --git a/services/Bff/src/Bff.Api/Auth/AccessPolicy.cs b/services/Bff/src/Bff.Api/Auth/AccessPolicy.cs new file mode 100644 index 00000000..e4036fe8 --- /dev/null +++ b/services/Bff/src/Bff.Api/Auth/AccessPolicy.cs @@ -0,0 +1,60 @@ +namespace Bff.Api.Auth; + +public enum AccessRequirement +{ + /// No session required (health, auth endpoints). + Anonymous, + + /// Any authenticated role, including read-only (viewer/client-readonly). + Read, + + /// Mutating role required: admin/editor/analyst (TS requireApiAuth). + Mutate, + + /// Chat: allows client-readonly, blocks viewer (TS requireApiAuthForChat). + Chat, +} + +/// +/// Single source of truth for the per-route access policy — the result of the per-route audit +/// the plan calls for. This replaces the 79 scattered forbiddenIfNotLocal guards + 20 requireApiAuth +/// calls in the Next.js routes. The localhost guard is intentionally dropped: under the new topology +/// it is subsumed by auth + the upstreams being network-internal. +/// +/// Default convention (refine specific paths here as needed): +/// - GET/HEAD under /api -> Read (reads were open behind localhost before; now require a session) +/// - other methods /api -> Mutate (mirrors the dominant requireApiAuth pattern) +/// - chat / auth / health -> explicit overrides below +/// +public static class AccessPolicy +{ + public static AccessRequirement Resolve(string method, PathString path) + { + // Non-/api paths (swagger/docs/health) are open. + if (!path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase)) + { + return AccessRequirement.Anonymous; + } + + // Health + auth handshake endpoints. + if (Matches(path, "/api/health") + || Matches(path, "/api/auth/login") + || Matches(path, "/api/auth/session") + || Matches(path, "/api/auth/logout")) + { + return AccessRequirement.Anonymous; + } + + // Chat is a read-only query but allows client-readonly. + if (Matches(path, "/api/chat") || Matches(path, "/api/chat/")) + { + return AccessRequirement.Chat; + } + + var isRead = HttpMethods.IsGet(method) || HttpMethods.IsHead(method) || HttpMethods.IsOptions(method); + return isRead ? AccessRequirement.Read : AccessRequirement.Mutate; + } + + private static bool Matches(PathString path, string value) => + path.Equals(value, StringComparison.OrdinalIgnoreCase); +} diff --git a/services/Bff/src/Bff.Api/Auth/WpSessionAuthenticationHandler.cs b/services/Bff/src/Bff.Api/Auth/WpSessionAuthenticationHandler.cs new file mode 100644 index 00000000..86746aec --- /dev/null +++ b/services/Bff/src/Bff.Api/Auth/WpSessionAuthenticationHandler.cs @@ -0,0 +1,63 @@ +using System.Security.Claims; +using System.Text.Encodings.Web; +using Bff.Application.Auth; +using Bff.Application.Options; +using Bff.Domain; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; + +namespace Bff.Api.Auth; + +public static class WpSessionDefaults +{ + public const string Scheme = "WpSession"; + public const string AuthDisabledClaim = "wp:auth_disabled"; +} + +/// +/// Authenticates requests from the wp_session cookie (verified byte-compatibly with auth.ts). +/// When auth is disabled (no AUTH_SECRET), every request is authenticated as the default role, +/// mirroring the TS behaviour where authEnabled() === false permits everything. +/// +public sealed class WpSessionAuthenticationHandler : AuthenticationHandler +{ + private readonly AuthOptions _auth; + + public WpSessionAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + IOptions auth) + : base(options, logger, encoder) + { + _auth = auth.Value; + } + + protected override Task HandleAuthenticateAsync() + { + if (!_auth.Enabled) + { + return Task.FromResult(AuthenticateResult.Success(BuildTicket(_auth.DefaultRole, authDisabled: true))); + } + + var cookie = Request.Cookies[WpSessionTokens.CookieName]; + var role = WpSessionTokens.VerifyRole(cookie, _auth.Secret, DateTimeOffset.UtcNow.ToUnixTimeSeconds()); + if (string.IsNullOrEmpty(role)) + { + return Task.FromResult(AuthenticateResult.NoResult()); + } + return Task.FromResult(AuthenticateResult.Success(BuildTicket(role, authDisabled: false))); + } + + private AuthenticationTicket BuildTicket(string role, bool authDisabled) + { + var claims = new List { new(ClaimTypes.Role, role) }; + if (authDisabled) + { + claims.Add(new Claim(WpSessionDefaults.AuthDisabledClaim, "true")); + } + var identity = new ClaimsIdentity(claims, WpSessionDefaults.Scheme); + var principal = new ClaimsPrincipal(identity); + return new AuthenticationTicket(principal, WpSessionDefaults.Scheme); + } +} diff --git a/services/Bff/src/Bff.Api/Bff.Api.csproj b/services/Bff/src/Bff.Api/Bff.Api.csproj new file mode 100644 index 00000000..59624093 --- /dev/null +++ b/services/Bff/src/Bff.Api/Bff.Api.csproj @@ -0,0 +1,18 @@ + + + + + + + + + + + + + net10.0 + enable + enable + + + diff --git a/services/Bff/src/Bff.Api/Endpoints/AuthEndpoints.cs b/services/Bff/src/Bff.Api/Endpoints/AuthEndpoints.cs new file mode 100644 index 00000000..a4f3b958 --- /dev/null +++ b/services/Bff/src/Bff.Api/Endpoints/AuthEndpoints.cs @@ -0,0 +1,132 @@ +using System.Security.Cryptography; +using System.Text; +using Bff.Application.Auth; +using Bff.Application.Options; +using Bff.Domain; +using Microsoft.Extensions.Options; + +namespace Bff.Api.Endpoints; + +/// +/// Auth handshake endpoints, moved into the BFF (it now owns setting/verifying the wp_session cookie). +/// Mirrors web/app/api/auth/login + auth/session. +/// +public static class AuthEndpoints +{ + public static void MapAuthEndpoints(this IEndpointRouteBuilder app) + { + app.MapPost("/api/auth/login", (HttpContext context, IOptions authOptions) => + { + var auth = authOptions.Value; + if (!auth.Enabled) + { + return Results.Json(new { ok = true, auth = "disabled" }); + } + if (!ParseBasicAuth(context, auth)) + { + return Results.Json(new { error = "Invalid credentials" }, statusCode: StatusCodes.Status401Unauthorized); + } + + var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + var token = WpSessionTokens.Create(auth.DefaultRole, auth.Secret, now, auth.SessionMaxAgeSeconds); + SetSessionCookie(context, token, auth); + return Results.Json(new { ok = true }); + }); + + app.MapPost("/api/auth/logout", (HttpContext context, IOptions authOptions) => + { + var auth = authOptions.Value; + context.Response.Cookies.Append(WpSessionTokens.CookieName, string.Empty, new CookieOptions + { + HttpOnly = true, + SameSite = ParseSameSite(auth.CookieSameSite), + Secure = auth.CookieSecure || ParseSameSite(auth.CookieSameSite) == SameSiteMode.None, + Path = "/", + Expires = DateTimeOffset.UnixEpoch, + Domain = string.IsNullOrEmpty(auth.CookieDomain) ? null : auth.CookieDomain, + }); + return Results.Json(new { ok = true }); + }); + + app.MapGet("/api/auth/session", (HttpContext context, IOptions authOptions) => + { + var auth = authOptions.Value; + var enabled = auth.Enabled; + var role = enabled + ? WpSessionTokens.VerifyRole( + context.Request.Cookies[WpSessionTokens.CookieName], + auth.Secret, + DateTimeOffset.UtcNow.ToUnixTimeSeconds()) + : null; + var effective = role ?? (enabled ? null : Roles.Analyst); + return Results.Json(new + { + authEnabled = enabled, + authenticated = !enabled || role is not null, + role = effective, + canMutate = Roles.CanMutate(effective), + @readonly = enabled && role is not null && !Roles.CanMutate(role), + }); + }); + } + + private static bool ParseBasicAuth(HttpContext context, AuthOptions auth) + { + if (string.IsNullOrEmpty(auth.BasicPassword)) + { + return false; + } + var header = context.Request.Headers.Authorization.ToString(); + if (!header.StartsWith("Basic ", StringComparison.Ordinal)) + { + return false; + } + try + { + var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(header[6..])); + var idx = decoded.IndexOf(':'); + if (idx < 0) + { + return false; + } + // Split on the first colon only (RFC 7617: password may contain colons). + var user = decoded[..idx]; + var pass = decoded[(idx + 1)..]; + // Constant-time compare (matches WpSessionTokens' HMAC check). Hashing + // to a fixed length first avoids leaking credential length, and `&` + // (not `&&`) ensures both comparisons always run. + return FixedTimeStringEquals(user, auth.BasicUser) + & FixedTimeStringEquals(pass, auth.BasicPassword); + } + catch (FormatException) + { + return false; + } + } + + private static bool FixedTimeStringEquals(string a, string b) => + CryptographicOperations.FixedTimeEquals( + SHA256.HashData(Encoding.UTF8.GetBytes(a)), + SHA256.HashData(Encoding.UTF8.GetBytes(b))); + + private static void SetSessionCookie(HttpContext context, string token, AuthOptions auth) + { + var sameSite = ParseSameSite(auth.CookieSameSite); + context.Response.Cookies.Append(WpSessionTokens.CookieName, token, new CookieOptions + { + HttpOnly = true, + SameSite = sameSite, + Secure = auth.CookieSecure || sameSite == SameSiteMode.None, + Path = "/", + MaxAge = TimeSpan.FromSeconds(auth.SessionMaxAgeSeconds), + Domain = string.IsNullOrEmpty(auth.CookieDomain) ? null : auth.CookieDomain, + }); + } + + private static SameSiteMode ParseSameSite(string value) => value.Trim().ToLowerInvariant() switch + { + "none" => SameSiteMode.None, + "strict" => SameSiteMode.Strict, + _ => SameSiteMode.Lax, + }; +} diff --git a/services/Bff/src/Bff.Api/Endpoints/ProxyEndpoints.cs b/services/Bff/src/Bff.Api/Endpoints/ProxyEndpoints.cs new file mode 100644 index 00000000..9ada56e8 --- /dev/null +++ b/services/Bff/src/Bff.Api/Endpoints/ProxyEndpoints.cs @@ -0,0 +1,143 @@ +using Bff.Api.Forwarding; +using Bff.Application; +using Bff.Application.Options; +using Microsoft.Extensions.Options; + +namespace Bff.Api.Endpoints; + +/// +/// The reverse-proxy surface: every /api/* request is mirrored to FastAPI, with explicit +/// handling for streaming (chat SSE) and for exports that translate to the FileService. +/// All near-identical 1:1 routes collapse into one catch-all instead of ~84 hand-written files. +/// +public static class ProxyEndpoints +{ + public static void MapProxyEndpoints(this IEndpointRouteBuilder app) + { + // Chat: Server-Sent Events stream to FastAPI (upstream route has a trailing slash). + app.MapPost("/api/chat", (HttpContext ctx) => (IResult)new ForwardingResult( + DependencyInjection.FastApiStreamClient, + $"/api/chat/{ctx.Request.QueryString}", + disableResponseBuffering: true)); + + // Report export: PDF/CSV/JSON are all rendered by the FileService (which reads Postgres + // directly). A missing format defaults to csv (matches the old Python default); any other + // format is rejected (the Python export route has been removed). Mirrors proxyToFileService.ts. + app.MapGet("/api/report/export", (HttpContext ctx) => + { + var raw = ctx.Request.Query["format"].ToString(); + var format = string.IsNullOrEmpty(raw) ? "csv" : raw.ToLowerInvariant(); + if (format is not ("pdf" or "csv" or "json")) + { + return Results.Json( + new { error = $"Unsupported export format '{format}'. Use pdf, csv, or json." }, + statusCode: 400); + } + var path = BuildFileServiceReportPath(ctx.Request.Query, format); + return path is null + ? Results.Json(new { error = "reportId or domain required for export" }, statusCode: 400) + : (IResult)new ForwardingResult(DependencyInjection.FileServiceClient, path, disableResponseBuffering: true); + }); + + // Excel workbook export -> FileService. Mirrors proxyToFileService.ts. + app.MapGet("/api/report/export-workbook", (HttpContext ctx) => + { + var path = BuildFileServiceWorkbookPath(ctx.Request.Query); + return path is null + ? Results.Json(new { error = "reportId or domain required for workbook export" }, statusCode: 400) + : (IResult)new ForwardingResult(DependencyInjection.FileServiceClient, path, disableResponseBuffering: true); + }); + + // Sitemap export -> FileService (was Python via the catch-all; now rendered from Postgres). + app.MapGet("/api/report/export-sitemap", (HttpContext ctx) => + { + var path = BuildFileServiceReportPath(ctx.Request.Query, "sitemap"); + return path is null + ? Results.Json(new { error = "reportId or domain required for sitemap export" }, statusCode: 400) + : (IResult)new ForwardingResult(DependencyInjection.FileServiceClient, path, disableResponseBuffering: true); + }); + + // Catch-all: every other /api/* request -> FastAPI (streamed for remaining export routes), + // except paths in the DATA_ROUTES allowlist, which go to the internal Data service + // (GET reads + POST/PUT/DELETE mutations on matched prefixes). + // Auth still runs in AccessControlMiddleware before this delegate, so routing here doesn't + // change which roles may reach a path. Empty allowlist => nothing matches => all FastAPI. + app.Map("/api/{**rest}", (HttpContext ctx) => + { + var path = ctx.Request.Path.Value ?? string.Empty; + var streaming = path.Contains("/export", StringComparison.OrdinalIgnoreCase); + + var upstream = ctx.RequestServices.GetRequiredService>().Value; + var matchesDataRoute = upstream.DataRoutes.Any(prefix => + path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); + var toData = !streaming + && matchesDataRoute + && (HttpMethods.IsGet(ctx.Request.Method) + || HttpMethods.IsHead(ctx.Request.Method) + || HttpMethods.IsPost(ctx.Request.Method) + || HttpMethods.IsPut(ctx.Request.Method) + || HttpMethods.IsDelete(ctx.Request.Method)); + + var client = toData + ? DependencyInjection.DataClient + : streaming ? DependencyInjection.FastApiStreamClient : DependencyInjection.FastApiClient; + + return (IResult)new ForwardingResult( + client, + $"{ctx.Request.Path}{ctx.Request.QueryString}", + disableResponseBuffering: streaming); + }); + } + + // Builds the FileService path for a report export. pdf carries profile/branding; csv/json/sitemap + // only need disposition. Returns null when neither reportId nor domain is supplied. + private static string? BuildFileServiceReportPath(IQueryCollection query, string format) + { + var reportId = query["reportId"].ToString(); + var domain = query["domain"].ToString(); + var disposition = Defaulted(query["disposition"].ToString(), "attachment"); + + string qs; + if (format == "pdf") + { + var profile = Defaulted(query["profile"].ToString(), "standard"); + var branding = Defaulted(query["branding"].ToString(), "true"); + qs = $"?profile={Uri.EscapeDataString(profile)}&disposition={Uri.EscapeDataString(disposition)}&branding={Uri.EscapeDataString(branding)}"; + } + else + { + qs = $"?disposition={Uri.EscapeDataString(disposition)}"; + } + + if (!string.IsNullOrEmpty(reportId)) + { + return $"/v1/reports/{Uri.EscapeDataString(reportId)}/{format}{qs}"; + } + if (!string.IsNullOrEmpty(domain)) + { + return $"/v1/reports/by-domain/{Uri.EscapeDataString(domain)}/{format}{qs}"; + } + return null; + } + + private static string? BuildFileServiceWorkbookPath(IQueryCollection query) + { + var reportId = query["reportId"].ToString(); + var domain = query["domain"].ToString(); + var disposition = Defaulted(query["disposition"].ToString(), "attachment"); + var qs = $"?disposition={Uri.EscapeDataString(disposition)}"; + + if (!string.IsNullOrEmpty(reportId)) + { + return $"/v1/reports/{Uri.EscapeDataString(reportId)}/workbook{qs}"; + } + if (!string.IsNullOrEmpty(domain)) + { + return $"/v1/reports/by-domain/{Uri.EscapeDataString(domain)}/workbook{qs}"; + } + return null; + } + + private static string Defaulted(string value, string fallback) => + string.IsNullOrEmpty(value) ? fallback : value; +} diff --git a/services/Bff/src/Bff.Api/Forwarding/ForwardingResult.cs b/services/Bff/src/Bff.Api/Forwarding/ForwardingResult.cs new file mode 100644 index 00000000..16d36968 --- /dev/null +++ b/services/Bff/src/Bff.Api/Forwarding/ForwardingResult.cs @@ -0,0 +1,16 @@ +namespace Bff.Api.Forwarding; + +/// An IResult that forwards the current request to a named upstream. +public sealed class ForwardingResult(string clientName, string pathAndQuery, bool disableResponseBuffering) : IResult +{ + public Task ExecuteAsync(HttpContext httpContext) + { + var forwarder = httpContext.RequestServices.GetRequiredService(); + return forwarder.ForwardAsync( + httpContext, + clientName, + pathAndQuery, + disableResponseBuffering, + httpContext.RequestAborted); + } +} diff --git a/services/Bff/src/Bff.Api/Forwarding/IUpstreamForwarder.cs b/services/Bff/src/Bff.Api/Forwarding/IUpstreamForwarder.cs new file mode 100644 index 00000000..c2a7b502 --- /dev/null +++ b/services/Bff/src/Bff.Api/Forwarding/IUpstreamForwarder.cs @@ -0,0 +1,17 @@ +namespace Bff.Api.Forwarding; + +/// +/// Generic reverse-proxy primitive: forwards the current request to a named upstream client +/// and streams the response back. Handles opaque JSON payloads, SSE, and binary exports +/// uniformly (the upstream Content-Type/Content-Disposition are preserved). Cookies are NOT +/// forwarded upstream — the BFF terminates auth. +/// +public interface IUpstreamForwarder +{ + Task ForwardAsync( + HttpContext context, + string clientName, + string pathAndQuery, + bool disableResponseBuffering, + CancellationToken cancellationToken); +} diff --git a/services/Bff/src/Bff.Api/Forwarding/UpstreamForwarder.cs b/services/Bff/src/Bff.Api/Forwarding/UpstreamForwarder.cs new file mode 100644 index 00000000..79806e37 --- /dev/null +++ b/services/Bff/src/Bff.Api/Forwarding/UpstreamForwarder.cs @@ -0,0 +1,74 @@ +using Microsoft.AspNetCore.Http.Features; + +namespace Bff.Api.Forwarding; + +public sealed class UpstreamForwarder(IHttpClientFactory factory) : IUpstreamForwarder +{ + public async Task ForwardAsync( + HttpContext context, + string clientName, + string pathAndQuery, + bool disableResponseBuffering, + CancellationToken cancellationToken) + { + var client = factory.CreateClient(clientName); + var target = new Uri(client.BaseAddress!, pathAndQuery); + + using var request = new HttpRequestMessage(new HttpMethod(context.Request.Method), target); + + if (HasBody(context.Request.Method)) + { + request.Content = new StreamContent(context.Request.Body); + if (!string.IsNullOrEmpty(context.Request.ContentType)) + { + request.Content.Headers.TryAddWithoutValidation("Content-Type", context.Request.ContentType); + } + } + + // Forward a minimal allowlist of request headers (never Host/Cookie). + foreach (var name in ForwardableRequestHeaders) + { + if (context.Request.Headers.TryGetValue(name, out var values)) + { + request.Headers.TryAddWithoutValidation(name, values.ToArray()); + } + } + + using var upstream = await client.SendAsync( + request, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken); + + context.Response.StatusCode = (int)upstream.StatusCode; + + if (upstream.Content.Headers.ContentType is not null) + { + context.Response.ContentType = upstream.Content.Headers.ContentType.ToString(); + } + if (upstream.Content.Headers.TryGetValues("Content-Disposition", out var disposition)) + { + context.Response.Headers["Content-Disposition"] = disposition.ToArray(); + } + // Pass redirects through (e.g. the Google OAuth consent/callback 302s). + if (upstream.Headers.Location is not null) + { + context.Response.Headers["Location"] = upstream.Headers.Location.ToString(); + } + + if (disableResponseBuffering) + { + context.Features.Get()?.DisableBuffering(); + } + + await using var stream = await upstream.Content.ReadAsStreamAsync(cancellationToken); + await stream.CopyToAsync(context.Response.Body, cancellationToken); + } + + private static readonly string[] ForwardableRequestHeaders = ["Accept", "Accept-Language"]; + + private static bool HasBody(string method) => + HttpMethods.IsPost(method) + || HttpMethods.IsPut(method) + || HttpMethods.IsPatch(method) + || HttpMethods.IsDelete(method); +} diff --git a/services/Bff/src/Bff.Api/Infrastructure/UpstreamExceptionHandler.cs b/services/Bff/src/Bff.Api/Infrastructure/UpstreamExceptionHandler.cs new file mode 100644 index 00000000..85a415ec --- /dev/null +++ b/services/Bff/src/Bff.Api/Infrastructure/UpstreamExceptionHandler.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Mvc; + +namespace Bff.Api.Infrastructure; + +/// +/// Right-sized error normalization: upstream connection failures → 502, upstream timeouts → 504, +/// anything else → 500, all as ProblemDetails. 4xx/422 bodies from FastAPI are NOT remapped here — +/// the forwarder passes them through verbatim so the frontend's existing validation parsing keeps working. +/// +public sealed class UpstreamExceptionHandler(ILogger logger) : IExceptionHandler +{ + public async ValueTask TryHandleAsync( + HttpContext context, + Exception exception, + CancellationToken cancellationToken) + { + // Once the response has started streaming we can't change the status — let it bubble. + if (context.Response.HasStarted) + { + return false; + } + + // Client disconnected: not our error to report. + if (exception is OperationCanceledException && context.RequestAborted.IsCancellationRequested) + { + return false; + } + + var (status, title) = exception switch + { + HttpRequestException => (StatusCodes.Status502BadGateway, "Upstream request failed"), + TaskCanceledException or TimeoutException => (StatusCodes.Status504GatewayTimeout, "Upstream timed out"), + _ => (StatusCodes.Status500InternalServerError, "Internal server error"), + }; + + logger.LogWarning(exception, "BFF upstream error ({Status}) for {Method} {Path}", + status, context.Request.Method, context.Request.Path); + + context.Response.StatusCode = status; + var problem = new ProblemDetails { Status = status, Title = title, Detail = exception.Message }; + await context.Response.WriteAsJsonAsync(problem, problem.GetType(), options: null, + contentType: "application/problem+json", cancellationToken); + return true; + } +} diff --git a/services/Bff/src/Bff.Api/Program.cs b/services/Bff/src/Bff.Api/Program.cs new file mode 100644 index 00000000..04b5fd5d --- /dev/null +++ b/services/Bff/src/Bff.Api/Program.cs @@ -0,0 +1,90 @@ +using Bff.Api.Auth; +using Bff.Api.Endpoints; +using Bff.Api.Forwarding; +using Bff.Api.Infrastructure; +using Bff.Application; +using Microsoft.AspNetCore.Authentication; +using Microsoft.OpenApi; + +const string CorsPolicy = "bff"; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddBffApplication(); +builder.Services.AddSingleton(); + +builder.Services + .AddAuthentication(WpSessionDefaults.Scheme) + .AddScheme(WpSessionDefaults.Scheme, null); +builder.Services.AddAuthorization(); + +builder.Services.AddProblemDetails(); +builder.Services.AddExceptionHandler(); + +var corsOrigins = ResolveCorsOrigins(builder.Configuration); +builder.Services.AddCors(options => options.AddPolicy(CorsPolicy, policy => + policy.WithOrigins(corsOrigins) + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials())); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(options => +{ + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "Website Profiling BFF", + Version = "v1", + Description = + "Backend-for-Frontend gateway: the single browser-facing API surface. Owns auth + CORS " + + "and proxies to the internal FastAPI and FileService backends.", + }); +}); + +// Large uploads (logs/upload, credentials/upload, page-markdown/extract) — parity with the TS proxy. +builder.WebHost.ConfigureKestrel(options => options.Limits.MaxRequestBodySize = 256L * 1024 * 1024); + +var app = builder.Build(); + +app.UseExceptionHandler(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/swagger/v1/swagger.json", "Website Profiling BFF v1"); + options.RoutePrefix = "docs"; + }); +} + +// CORS before auth so denied (401/403) responses still carry CORS headers for the browser. +app.UseCors(CorsPolicy); +app.UseAuthentication(); +app.UseMiddleware(); + +app.MapGet("/health", () => Results.Ok(new { status = "ok" })) + .WithName("HealthCheck") + .WithTags("Health"); + +app.MapAuthEndpoints(); +app.MapProxyEndpoints(); + +app.Run(); + +static string[] ResolveCorsOrigins(IConfiguration config) +{ + var env = Environment.GetEnvironmentVariable("BFF_ALLOWED_ORIGINS"); + if (!string.IsNullOrWhiteSpace(env)) + { + return env.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + var fromConfig = config.GetSection("Cors:AllowedOrigins").Get(); + if (fromConfig is { Length: > 0 }) + { + return fromConfig; + } + return ["http://localhost:3000"]; +} + +public partial class Program; diff --git a/services/Bff/src/Bff.Api/appsettings.Development.json b/services/Bff/src/Bff.Api/appsettings.Development.json new file mode 100644 index 00000000..308b3b5a --- /dev/null +++ b/services/Bff/src/Bff.Api/appsettings.Development.json @@ -0,0 +1,22 @@ +{ + "Upstream": { + "DataBaseUrl": "http://127.0.0.1:8091", + "DataRoutes": [ + "/api/report/meta", + "/api/report/payload", + "/api/report/history", + "/api/report/crawl-payload", + "/api/report/mobile-delta", + "/api/report/portfolio", + "/api/portfolio", + "/api/issues/status", + "/api/filters" + ] + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/services/Bff/src/Bff.Api/appsettings.json b/services/Bff/src/Bff.Api/appsettings.json new file mode 100644 index 00000000..c37353ca --- /dev/null +++ b/services/Bff/src/Bff.Api/appsettings.json @@ -0,0 +1,23 @@ +{ + "Urls": "http://127.0.0.1:8090", + "Upstream": { + "FastApiBaseUrl": "http://127.0.0.1:8001", + "FileServiceBaseUrl": "http://127.0.0.1:8080", + "TimeoutSeconds": 120 + }, + "Auth": { + "CookieSameSite": "Lax", + "CookieSecure": false, + "DefaultRole": "analyst" + }, + "Cors": { + "AllowedOrigins": ["http://localhost:3000"] + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/services/Bff/src/Bff.Application/Auth/WpSessionTokens.cs b/services/Bff/src/Bff.Application/Auth/WpSessionTokens.cs new file mode 100644 index 00000000..cade4c33 --- /dev/null +++ b/services/Bff/src/Bff.Application/Auth/WpSessionTokens.cs @@ -0,0 +1,148 @@ +using System.Globalization; +using System.Security.Cryptography; +using System.Text; + +namespace Bff.Application.Auth; + +/// +/// Create/verify the wp_session token, byte-for-byte compatible with the TypeScript +/// implementation in web/src/server/auth.ts. Compatibility is load-bearing: existing +/// sessions must survive the big-bang cutover, so any behavioural divergence (hex casing, +/// exp parsing, dot-splitting) would silently invalidate live cookies. +/// +/// Token format: "{role}:{exp}.{hmacSha256Hex(secret, "{role}:{exp}")}" +/// - hex is lowercase on creation, but verification is case-insensitive (Node Buffer.from +/// hex decoding is case-insensitive, so we decode bytes and compare). +/// - exp is parsed JS-parseInt style (leading numeric prefix), NOT strict integer parsing. +/// +public static class WpSessionTokens +{ + public const string CookieName = "wp_session"; + + /// HMAC-SHA256(secret, payload) as lowercase hex. Mirrors TS signToken(). + public static string Sign(string payload, string secret) + { + using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); + var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload)); + return Convert.ToHexStringLower(hash); + } + + /// Mirrors TS createSessionToken(): "{role}:{exp}.{sig}". Returns "" if no secret. + public static string Create(string role, string secret, long nowUnixSeconds, int maxAgeSeconds) + { + if (string.IsNullOrEmpty(secret)) + { + return string.Empty; + } + var exp = nowUnixSeconds + maxAgeSeconds; + var payload = $"{role}:{exp}"; + return $"{payload}.{Sign(payload, secret)}"; + } + + /// + /// Mirrors TS verifySessionToken(): returns the role if the token is valid and unexpired, + /// otherwise null. Returns null when the secret is empty (auth disabled is handled upstream). + /// + public static string? VerifyRole(string? token, string secret, long nowUnixSeconds) + { + if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(secret)) + { + return null; + } + + // TS: token.split('.') must yield exactly 2 parts. + var parts = token.Split('.'); + if (parts.Length != 2) + { + return null; + } + + var payload = parts[0]; + var sig = parts[1]; + var expectedHex = Sign(payload, secret); + + // TS compares decoded HMAC bytes with timingSafeEqual (and returns null on length mismatch). + // Node's hex decode is case-insensitive, so decode both and fixed-time compare the bytes. + byte[] sigBytes; + byte[] expectedBytes; + try + { + sigBytes = Convert.FromHexString(sig); + expectedBytes = Convert.FromHexString(expectedHex); + } + catch (FormatException) + { + return null; + } + if (sigBytes.Length != expectedBytes.Length || + !CryptographicOperations.FixedTimeEquals(sigBytes, expectedBytes)) + { + return null; + } + + // TS: const [role, expStr] = payload.split(':'); + var seg = payload.Split(':'); + var role = seg.Length > 0 ? seg[0] : null; + var expStr = seg.Length > 1 ? seg[1] : null; + + // TS: const exp = parseInt(expStr || '0', 10); (NaN if no leading digits) + var exp = JsParseInt(string.IsNullOrEmpty(expStr) ? "0" : expStr); + + // TS: if (!role || !Number.isFinite(exp) || exp < now) return null; + if (string.IsNullOrEmpty(role) || exp is null || exp.Value < nowUnixSeconds) + { + return null; + } + return role; + } + + /// + /// JavaScript parseInt(str, 10) semantics: skip leading whitespace, optional sign, then + /// consume the leading run of decimal digits; ignore trailing garbage. Returns null for NaN + /// (no digits) — the .NET stand-in for !Number.isFinite. This is the off-by-one fix called out + /// in the plan: long.TryParse("123abc") fails, but JS parseInt("123abc") === 123. + /// + public static long? JsParseInt(string? input) + { + if (input is null) + { + return null; + } + + var i = 0; + var n = input.Length; + while (i < n && char.IsWhiteSpace(input[i])) + { + i++; + } + + var sign = 1L; + if (i < n && (input[i] == '+' || input[i] == '-')) + { + if (input[i] == '-') + { + sign = -1L; + } + i++; + } + + var start = i; + while (i < n && input[i] >= '0' && input[i] <= '9') + { + i++; + } + if (i == start) + { + return null; // no digits -> NaN + } + + var digits = input.Substring(start, i - start); + // Guard against absurdly long digit runs overflowing long; JS would keep precision as + // a double, but exp values here are 10-digit unix seconds, so long is sufficient. + if (!long.TryParse(digits, NumberStyles.None, CultureInfo.InvariantCulture, out var value)) + { + return long.MaxValue * sign; // overflow -> treat as a finite, far-future/past value + } + return sign * value; + } +} diff --git a/services/Bff/src/Bff.Application/Bff.Application.csproj b/services/Bff/src/Bff.Application/Bff.Application.csproj new file mode 100644 index 00000000..de38ac1e --- /dev/null +++ b/services/Bff/src/Bff.Application/Bff.Application.csproj @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + net10.0 + enable + enable + + + diff --git a/services/Bff/src/Bff.Application/DependencyInjection.cs b/services/Bff/src/Bff.Application/DependencyInjection.cs new file mode 100644 index 00000000..668173d2 --- /dev/null +++ b/services/Bff/src/Bff.Application/DependencyInjection.cs @@ -0,0 +1,160 @@ +using Bff.Application.Http; +using Bff.Application.Options; +using Microsoft.Extensions.DependencyInjection; + +namespace Bff.Application; + +public static class DependencyInjection +{ + /// Named HttpClient for normal JSON proxying to FastAPI (with idempotent retry). + public const string FastApiClient = "fastapi"; + + /// Named HttpClient for streaming proxying to FastAPI (SSE/exports) — NO retry/buffering. + public const string FastApiStreamClient = "fastapi-stream"; + + /// Named HttpClient for the FileService (PDF/Excel exports) — streaming, no retry. + public const string FileServiceClient = "fileservice"; + + /// Named HttpClient for the internal Data service (direct-Postgres reads) — idempotent retry. + public const string DataClient = "data"; + + public static IServiceCollection AddBffApplication(this IServiceCollection services) + { + services.AddOptions() + .BindConfiguration(UpstreamOptions.SectionName) + .PostConfigure(o => + { + var fastapi = Environment.GetEnvironmentVariable("FASTAPI_URL"); + if (!string.IsNullOrWhiteSpace(fastapi)) + { + o.FastApiBaseUrl = fastapi.Trim(); + } + var files = Environment.GetEnvironmentVariable("FILE_SERVICE_URL"); + if (!string.IsNullOrWhiteSpace(files)) + { + o.FileServiceBaseUrl = files.Trim(); + } + var data = Environment.GetEnvironmentVariable("DATA_SERVICE_URL"); + if (!string.IsNullOrWhiteSpace(data)) + { + o.DataBaseUrl = data.Trim(); + } + var dataRoutes = Environment.GetEnvironmentVariable("DATA_ROUTES"); + if (!string.IsNullOrWhiteSpace(dataRoutes)) + { + o.DataRoutes = dataRoutes + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + }); + + services.AddOptions() + .BindConfiguration(AuthOptions.SectionName) + .PostConfigure(o => + { + var secret = Environment.GetEnvironmentVariable("AUTH_SECRET") + ?? Environment.GetEnvironmentVariable("SESSION_SECRET"); + if (!string.IsNullOrWhiteSpace(secret)) + { + o.Secret = secret.Trim(); + } + var user = Environment.GetEnvironmentVariable("AUTH_USER"); + if (!string.IsNullOrWhiteSpace(user)) + { + o.BasicUser = user.Trim(); + } + var pass = Environment.GetEnvironmentVariable("AUTH_PASSWORD"); + if (pass is not null) + { + o.BasicPassword = pass.Trim(); + } + var role = Environment.GetEnvironmentVariable("AUTH_DEFAULT_ROLE"); + if (!string.IsNullOrWhiteSpace(role)) + { + o.DefaultRole = role.Trim(); + } + var sameSite = Environment.GetEnvironmentVariable("BFF_COOKIE_SAMESITE"); + if (!string.IsNullOrWhiteSpace(sameSite)) + { + o.CookieSameSite = sameSite.Trim(); + } + var secure = Environment.GetEnvironmentVariable("BFF_COOKIE_SECURE"); + if (!string.IsNullOrWhiteSpace(secure)) + { + o.CookieSecure = secure.Trim().Equals("true", StringComparison.OrdinalIgnoreCase); + } + var domain = Environment.GetEnvironmentVariable("BFF_COOKIE_DOMAIN"); + if (!string.IsNullOrWhiteSpace(domain)) + { + o.CookieDomain = domain.Trim(); + } + }); + + services.AddOptions() + .BindConfiguration(BffCorsOptions.SectionName) + .PostConfigure(o => + { + var origins = Environment.GetEnvironmentVariable("BFF_ALLOWED_ORIGINS"); + if (!string.IsNullOrWhiteSpace(origins)) + { + o.AllowedOrigins = origins + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + }); + + services.AddTransient(); + + services.AddHttpClient(FastApiClient) + .ConfigureHttpClient((sp, client) => + { + var opts = GetUpstream(sp); + client.BaseAddress = NormalizeBase(opts.FastApiBaseUrl); + client.Timeout = TimeSpan.FromSeconds(Math.Max(5, opts.TimeoutSeconds)); + }) + .AddHttpMessageHandler(); + + // Internal Data service (direct-Postgres reads + issue/portfolio/filter mutations). + // GET/HEAD retry is safe; POST/PUT/DELETE are forwarded without retry. + services.AddHttpClient(DataClient) + .ConfigureHttpClient((sp, client) => + { + var opts = GetUpstream(sp); + client.BaseAddress = NormalizeBase(opts.DataBaseUrl); + client.Timeout = TimeSpan.FromSeconds(Math.Max(5, opts.TimeoutSeconds)); + }) + .AddHttpMessageHandler(); + + services.AddHttpClient(FastApiStreamClient) + .ConfigureHttpClient((sp, client) => + { + client.BaseAddress = NormalizeBase(GetUpstream(sp).FastApiBaseUrl); + client.Timeout = Timeout.InfiniteTimeSpan; // SSE/streaming: do not cut the body + }); + + services.AddHttpClient(FileServiceClient) + .ConfigureHttpClient((sp, client) => + { + var opts = GetUpstream(sp); + client.BaseAddress = NormalizeBase(opts.FileServiceBaseUrl); + client.Timeout = TimeSpan.FromSeconds(Math.Max(5, opts.TimeoutSeconds)); + }); + + // Typed FastAPI client generated from web/openapi.json (NSwag). The bulk of the gateway + // proxies opaque payloads via the generic forwarder; this typed client is available for + // aggregation/composition endpoints that need to read upstream responses by shape. + services.AddHttpClient() + .ConfigureHttpClient((sp, client) => + { + var opts = GetUpstream(sp); + client.BaseAddress = NormalizeBase(opts.FastApiBaseUrl); + client.Timeout = TimeSpan.FromSeconds(Math.Max(5, opts.TimeoutSeconds)); + }) + .AddHttpMessageHandler(); + + return services; + } + + private static UpstreamOptions GetUpstream(IServiceProvider sp) => + sp.GetRequiredService>().Value; + + private static Uri NormalizeBase(string url) => new(url.TrimEnd('/') + "/"); +} diff --git a/services/Bff/src/Bff.Application/Generated/FastApiClient.g.cs b/services/Bff/src/Bff.Application/Generated/FastApiClient.g.cs new file mode 100644 index 00000000..afe2dcdb --- /dev/null +++ b/services/Bff/src/Bff.Application/Generated/FastApiClient.g.cs @@ -0,0 +1,13145 @@ +//---------------------- +// +// Generated using the NSwag toolchain v14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) +// +//---------------------- + +#nullable enable + +#pragma warning disable 108 // Disable "CS0108 '{derivedDto}.ToJson()' hides inherited member '{dtoBase}.ToJson()'. Use the new keyword if hiding was intended." +#pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword." +#pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?' +#pragma warning disable 612 // Disable "CS0612 '...' is obsolete" +#pragma warning disable 649 // Disable "CS0649 Field is never assigned to, and will always have its default value null" +#pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ... +#pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..." +#pragma warning disable 8073 // Disable "CS8073 The result of the expression is always 'false' since a value of type 'T' is never equal to 'null' of type 'T?'" +#pragma warning disable 3016 // Disable "CS3016 Arrays as attribute arguments is not CLS-compliant" +#pragma warning disable 8600 // Disable "CS8600 Converting null literal or possible null value to non-nullable type" +#pragma warning disable 8602 // Disable "CS8602 Dereference of a possibly null reference" +#pragma warning disable 8603 // Disable "CS8603 Possible null reference return" +#pragma warning disable 8604 // Disable "CS8604 Possible null reference argument for parameter" +#pragma warning disable 8625 // Disable "CS8625 Cannot convert null literal to non-nullable reference type" +#pragma warning disable 8765 // Disable "CS8765 Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes)." + +namespace Bff.Application.Generated +{ + using System = global::System; + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial interface IFastApiClient + { + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Health Check + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Health_check_api_health_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Report Meta + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Report_meta_api_report_meta_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Report Payload + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Report_payload_api_report_payload_getAsync(Reportid? reportId = null, Domain? domain = null, Section? section = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Report History + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Report_history_api_report_history_getAsync(Propertyid? propertyId = null, Anonymous? domain = null, int? limit = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Crawl Payload + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Crawl_payload_api_report_crawl_payload_getAsync(Crawlrunid? crawlRunId = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Mobile Delta + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Mobile_delta_api_report_mobile_delta_getAsync(Id? id = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Run Pipeline + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Run_pipeline_api_run_postAsync(RunPostBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// List Pipeline Jobs + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task List_pipeline_jobs_api_jobs_getAsync(int? limit = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Pipeline Job + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Get_pipeline_job_api_jobs__job_id__getAsync(string job_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Cancel Pipeline Job + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Cancel_pipeline_job_api_jobs__job_id__cancel_postAsync(string job_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Pause Pipeline Job + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Pause_pipeline_job_api_jobs__job_id__pause_postAsync(string job_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Resume Pipeline Job + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Resume_pipeline_job_api_jobs__job_id__resume_postAsync(string job_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Chat Turn + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Chat_turn_api_chat__postAsync(ChatRequest body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// List Sessions + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task List_sessions_api_chat_sessions_getAsync(int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Create Session + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Create_session_api_chat_sessions_postAsync(ChatSessionCreate body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Session Route + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Get_session_route_api_chat_sessions__session_id__getAsync(int session_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Delete Session Route + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Delete_session_route_api_chat_sessions__session_id__deleteAsync(int session_id, int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Session Messages + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Get_session_messages_api_chat_sessions__session_id__messages_getAsync(int session_id, int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Artifact + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Get_artifact_api_chat_artifacts__artifact_id__getAsync(string artifact_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Browser Status Check + /// + /// + /// Return whether Playwright + Chromium are available. + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Browser_status_check_api_crawl_browser_status_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Page Html + /// + /// + /// Return stored HTML and metadata for a URL within a crawl run. + /// + /// Page URL to retrieve stored HTML for + /// Crawl run ID + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Get_page_html_api_crawl_page_html_getAsync(string url, Anonymous2? crawlRunId = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Pipeline Config + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Get_pipeline_config_api_pipeline_config_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Put Pipeline Config + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Put_pipeline_config_api_pipeline_config_putAsync(PipelineConfigBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Llm Config + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Get_llm_config_api_llm_config_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Put Llm Config + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Put_llm_config_api_llm_config_putAsync(LlmConfigBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Secrets + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Get_secrets_api_secrets_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Put Secrets + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Put_secrets_api_secrets_putAsync(SecretsBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get App Setting + /// + /// Settings key to retrieve + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Get_app_setting_api_app_settings_getAsync(string key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Put App Setting + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Put_app_setting_api_app_settings_putAsync(AppSettingBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// List Properties + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task List_properties_api_properties_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Create Property + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Create_property_api_properties_postAsync(PropertyUpsertBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Resolve Property + /// + /// Start URL to resolve a property from + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Resolve_property_api_properties_resolve_getAsync(string startUrl, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Property + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Get_property_api_properties__property_id__getAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Delete Property Route + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Delete_property_route_api_properties__property_id__deleteAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Property Ops Route + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Get_property_ops_route_api_properties__property_id__ops_getAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Update Property Ops Route + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Update_property_ops_route_api_properties__property_id__ops_putAsync(int property_id, OpsSettingsBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Property Preset + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Get_property_preset_api_properties__property_id__preset_getAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Update Property Preset + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Update_property_preset_api_properties__property_id__preset_putAsync(int property_id, PresetBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Authorize Property Crawl Route + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Authorize_property_crawl_route_api_properties__property_id__authorize_postAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Property Google Status + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Property_google_status_api_properties__property_id__google_status_getAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Property Google Test + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Property_google_test_api_properties__property_id__google_test_postAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Property Google Properties + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Property_google_properties_api_properties__property_id__google_properties_getAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Property Google Links Status + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Property_google_links_status_api_properties__property_id__google_links_status_getAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Property Google Links Import + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Property_google_links_import_api_properties__property_id__google_links_import_postAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Patch Property Google Credentials + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Patch_property_google_credentials_api_properties__property_id__google_credentials_patchAsync(int property_id, GoogleCredentialsPatch body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Post Property Google Credentials + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Post_property_google_credentials_api_properties__property_id__google_credentials_postAsync(int property_id, GoogleCredentialsPostBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Post Property Google Disconnect + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Post_property_google_disconnect_api_properties__property_id__google_disconnect_postAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// List Dashboards + /// + /// Property ID + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task List_dashboards_api_dashboards_getAsync(int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Create Dashboard + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Create_dashboard_api_dashboards_postAsync(DashboardCreateBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Dashboard + /// + /// Property ID + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Get_dashboard_api_dashboards__dashboard_id__getAsync(int dashboard_id, int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Update Dashboard + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Update_dashboard_api_dashboards__dashboard_id__putAsync(int dashboard_id, DashboardUpdateBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Delete Dashboard + /// + /// Property ID + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Delete_dashboard_api_dashboards__dashboard_id__deleteAsync(int dashboard_id, int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Dashboards Ai Generate + /// + /// + /// Generate DashScript, a widget, or a full dashboard via LLM. + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Dashboards_ai_generate_api_dashboards_ai_generate_postAsync(DashboardAiGenerateBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// List Filters + /// + /// Property ID + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task List_filters_api_filters_getAsync(int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Upsert Filter + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Upsert_filter_api_filters_postAsync(FilterUpsertBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Delete Filter + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Delete_filter_api_filters_deleteAsync(FilterDeleteBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Google Credentials + /// + /// + /// Full app-level Google OAuth settings (server-side / local admin only). + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Get_google_credentials_api_integrations_google_credentials_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Save Google Credentials + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Save_google_credentials_api_integrations_google_credentials_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Status + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Google_status_api_integrations_google_status_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Upload Google Credentials + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Upload_google_credentials_api_integrations_google_credentials_upload_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Disconnect + /// + /// + /// Global disconnect is deprecated — use per-property disconnect. + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Google_disconnect_api_integrations_google_disconnect_postAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Oauth Start + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Google_oauth_start_api_integrations_google_auth_getAsync(Anonymous3? propertyId = null, Starturl? startUrl = null, Returnto? returnTo = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Oauth Callback + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Google_oauth_callback_api_integrations_google_callback_getAsync(Code? code = null, State? state = null, Error? error = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Properties Deprecated + /// + /// + /// Deprecated — use /api/properties/{id}/google/properties. + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Google_properties_deprecated_api_integrations_google_properties_getAsync(Anonymous4? propertyId = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Test + /// + /// + /// Run `python -m src google --test` and return stdout log. + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Google_test_api_integrations_google_test_postAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Page Data + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Google_page_data_api_integrations_google_page_data_getAsync(string url, Googlesnapshotid? googleSnapshotId = null, Anonymous5? propertyId = null, Anonymous6? domain = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Page Data History + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Google_page_data_history_api_integrations_google_page_data_history_getAsync(string url, Anonymous7? propertyId = null, Anonymous8? domain = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Page Live + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Google_page_live_api_integrations_google_page_live_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Keywords By Page + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Google_keywords_by_page_api_integrations_google_keywords_by_page_getAsync(string url, Anonymous9? propertyId = null, Anonymous10? domain = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Keywords History + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Google_keywords_history_api_integrations_google_keywords_history_getAsync(string keyword, Anonymous11? propertyId = null, Anonymous12? domain = null, int? limit = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Bing Sync + /// + /// + /// Fetch Bing Webmaster backlinks summary using config from DB. + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Bing_sync_api_integrations_bing_sync_postAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Page Compare + /// + /// + /// Compare two page Google data snapshots. + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Google_page_compare_api_integrations_google_page_compare_getAsync(string url, int currentId, int baselineId, string? currentType = null, string? baselineType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Page Live History + /// + /// + /// Return history of page Google snapshots for a URL. + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Google_page_live_history_api_integrations_google_page_live_history_getAsync(string url, int? limit = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Keywords History Batch + /// + /// + /// Batch keyword history: { keywords: str[], limit?: int, propertyId?: int, domain?: str } + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Google_keywords_history_batch_api_integrations_google_keywords_history_batch_postAsync(object body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Keywords Expand + /// + /// + /// Expand keyword ideas from Google Keyword Planner or suggest API. + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Google_keywords_expand_api_integrations_google_keywords_expand_postAsync(object body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Keywords Planner + /// + /// + /// Fetch keyword planner data from Google Ads API. + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Google_keywords_planner_api_integrations_google_keywords_planner_postAsync(object body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// List Issue Status Route + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task List_issue_status_route_api_issues_status_getAsync(int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Upsert Issue Status Route + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Upsert_issue_status_route_api_issues_status_putAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Issues Fix Suggestion + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Issues_fix_suggestion_api_issues_fix_suggestion_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Issues Action Plan + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Issues_action_plan_api_issues_action_plan_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Ai Fix Suggestion + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Ai_fix_suggestion_api_ai_fix_suggestion_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Keywords Competitor Import + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Keywords_competitor_import_api_keywords_competitor_import_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Keywords Content Brief + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Keywords_content_brief_api_keywords_content_brief_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Backlinks Velocity + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Backlinks_velocity_api_backlinks_velocity_getAsync(int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Backlinks Competitor Import + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Backlinks_competitor_import_api_backlinks_competitor_import_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Backlinks Third Party Import + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Backlinks_third_party_import_api_backlinks_third_party_import_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Content Analyze + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Content_analyze_api_content_analyze_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Content Score + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Content_score_api_content_score_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Content Wizard + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Content_wizard_api_content_wizard_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// List Content Drafts Route + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task List_content_drafts_route_api_content_drafts_getAsync(int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Create Content Draft Route + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Create_content_draft_route_api_content_drafts_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Content Draft Route + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Get_content_draft_route_api_content_drafts__draft_id__getAsync(int draft_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Update Content Draft Route + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Update_content_draft_route_api_content_drafts__draft_id__patchAsync(int draft_id, object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Delete Content Draft Route + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Delete_content_draft_route_api_content_drafts__draft_id__deleteAsync(int draft_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// List Page Markdown Route + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task List_page_markdown_route_api_page_markdown_getAsync(int crawlRunId, int? page = null, int? limit = null, Q? q = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Delete Page Markdown Route + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Delete_page_markdown_route_api_page_markdown_deleteAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Page Markdown Content Route + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Page_markdown_content_route_api_page_markdown_content_getAsync(int crawlRunId, string url, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Page Markdown Extract + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Page_markdown_extract_api_page_markdown_extract_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Page Markdown Runs Route + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Page_markdown_runs_route_api_page_markdown_runs_getAsync(Anonymous13? propertyId = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Ollama Status + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Ollama_status_api_ollama_status_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Mcp Tools + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Mcp_tools_api_mcp_tools_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Delete Portfolio Item + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Delete_portfolio_item_api_portfolio_delete_deleteAsync(DeletePortfolioBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Alerts Check + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Alerts_check_api_alerts_check_postAsync(int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Schedule Check + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Schedule_check_api_schedule_check_postAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Logs Upload + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Logs_upload_api_logs_upload_postAsync(int? propertyId = null, string? file = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Compare Export + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Compare_export_api_compare_export_postAsync(CompareExportBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Page Coach + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Page_coach_api_links_page_coach_postAsync(PageCoachBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Run Audit Tool + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Run_audit_tool_api_report_audit_tool_postAsync(AuditToolBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Export Report + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Export_report_api_report_export_getAsync(string? format = null, Anonymous14? reportId = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Export Sitemap + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Export_sitemap_api_report_export_sitemap_getAsync(Anonymous15? reportId = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Report Portfolio + /// + /// + /// Return portfolio data — groups, crawl history, summary, or single card. + /// + /// Successful Response + /// A server side error occurred. + System.Threading.Tasks.Task Report_portfolio_api_report_portfolio_getAsync(string? widget = null, Ids? ids = null, Anonymous16? reportId = null, Anonymous17? crawlRunId = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class FastApiClient : IFastApiClient + { + private System.Net.Http.HttpClient _httpClient; + private static System.Lazy _settings = new System.Lazy(CreateSerializerSettings, true); + private System.Text.Json.JsonSerializerOptions _instanceSettings; + + #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public FastApiClient(System.Net.Http.HttpClient httpClient) + #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + { + _httpClient = httpClient; + Initialize(); + } + + private static System.Text.Json.JsonSerializerOptions CreateSerializerSettings() + { + var settings = new System.Text.Json.JsonSerializerOptions(); + UpdateJsonSerializerSettings(settings); + return settings; + } + + protected System.Text.Json.JsonSerializerOptions JsonSerializerSettings { get { return _instanceSettings ?? _settings.Value; } } + + static partial void UpdateJsonSerializerSettings(System.Text.Json.JsonSerializerOptions settings); + + partial void Initialize(); + + partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, string url); + partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, System.Text.StringBuilder urlBuilder); + partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Health Check + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Health_check_api_health_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/health" + urlBuilder_.Append("api/health"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Report Meta + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Report_meta_api_report_meta_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/report/meta" + urlBuilder_.Append("api/report/meta"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Report Payload + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Report_payload_api_report_payload_getAsync(Reportid? reportId = null, Domain? domain = null, Section? section = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/report/payload" + urlBuilder_.Append("api/report/payload"); + urlBuilder_.Append('?'); + if (reportId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("reportId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(reportId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (domain != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("domain")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(domain, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (section != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("section")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(section, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Report History + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Report_history_api_report_history_getAsync(Propertyid? propertyId = null, Anonymous? domain = null, int? limit = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/report/history" + urlBuilder_.Append("api/report/history"); + urlBuilder_.Append('?'); + if (propertyId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("propertyId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(propertyId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (domain != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("domain")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(domain, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (limit != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("limit")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(limit, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Crawl Payload + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Crawl_payload_api_report_crawl_payload_getAsync(Crawlrunid? crawlRunId = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/report/crawl-payload" + urlBuilder_.Append("api/report/crawl-payload"); + urlBuilder_.Append('?'); + if (crawlRunId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("crawlRunId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(crawlRunId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Mobile Delta + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Mobile_delta_api_report_mobile_delta_getAsync(Id? id = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/report/mobile-delta" + urlBuilder_.Append("api/report/mobile-delta"); + urlBuilder_.Append('?'); + if (id != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("id")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(id, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Run Pipeline + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Run_pipeline_api_run_postAsync(RunPostBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/run" + urlBuilder_.Append("api/run"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// List Pipeline Jobs + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task List_pipeline_jobs_api_jobs_getAsync(int? limit = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/jobs" + urlBuilder_.Append("api/jobs"); + urlBuilder_.Append('?'); + if (limit != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("limit")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(limit, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Pipeline Job + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Get_pipeline_job_api_jobs__job_id__getAsync(string job_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (job_id == null) + throw new System.ArgumentNullException("job_id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/jobs/{job_id}" + urlBuilder_.Append("api/jobs/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(job_id, System.Globalization.CultureInfo.InvariantCulture))); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Cancel Pipeline Job + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Cancel_pipeline_job_api_jobs__job_id__cancel_postAsync(string job_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (job_id == null) + throw new System.ArgumentNullException("job_id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/jobs/{job_id}/cancel" + urlBuilder_.Append("api/jobs/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(job_id, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/cancel"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Pause Pipeline Job + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Pause_pipeline_job_api_jobs__job_id__pause_postAsync(string job_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (job_id == null) + throw new System.ArgumentNullException("job_id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/jobs/{job_id}/pause" + urlBuilder_.Append("api/jobs/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(job_id, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/pause"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Resume Pipeline Job + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Resume_pipeline_job_api_jobs__job_id__resume_postAsync(string job_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (job_id == null) + throw new System.ArgumentNullException("job_id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/jobs/{job_id}/resume" + urlBuilder_.Append("api/jobs/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(job_id, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/resume"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Chat Turn + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Chat_turn_api_chat__postAsync(ChatRequest body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/chat/" + urlBuilder_.Append("api/chat/"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// List Sessions + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task List_sessions_api_chat_sessions_getAsync(int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (propertyId == null) + throw new System.ArgumentNullException("propertyId"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/chat/sessions" + urlBuilder_.Append("api/chat/sessions"); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("propertyId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(propertyId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Create Session + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Create_session_api_chat_sessions_postAsync(ChatSessionCreate body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/chat/sessions" + urlBuilder_.Append("api/chat/sessions"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Session Route + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Get_session_route_api_chat_sessions__session_id__getAsync(int session_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (session_id == null) + throw new System.ArgumentNullException("session_id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/chat/sessions/{session_id}" + urlBuilder_.Append("api/chat/sessions/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(session_id, System.Globalization.CultureInfo.InvariantCulture))); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Delete Session Route + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Delete_session_route_api_chat_sessions__session_id__deleteAsync(int session_id, int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (session_id == null) + throw new System.ArgumentNullException("session_id"); + + if (propertyId == null) + throw new System.ArgumentNullException("propertyId"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("DELETE"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/chat/sessions/{session_id}" + urlBuilder_.Append("api/chat/sessions/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(session_id, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("propertyId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(propertyId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Session Messages + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Get_session_messages_api_chat_sessions__session_id__messages_getAsync(int session_id, int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (session_id == null) + throw new System.ArgumentNullException("session_id"); + + if (propertyId == null) + throw new System.ArgumentNullException("propertyId"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/chat/sessions/{session_id}/messages" + urlBuilder_.Append("api/chat/sessions/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(session_id, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/messages"); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("propertyId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(propertyId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Artifact + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Get_artifact_api_chat_artifacts__artifact_id__getAsync(string artifact_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (artifact_id == null) + throw new System.ArgumentNullException("artifact_id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/chat/artifacts/{artifact_id}" + urlBuilder_.Append("api/chat/artifacts/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(artifact_id, System.Globalization.CultureInfo.InvariantCulture))); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Browser Status Check + /// + /// + /// Return whether Playwright + Chromium are available. + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Browser_status_check_api_crawl_browser_status_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/crawl/browser-status" + urlBuilder_.Append("api/crawl/browser-status"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Page Html + /// + /// + /// Return stored HTML and metadata for a URL within a crawl run. + /// + /// Page URL to retrieve stored HTML for + /// Crawl run ID + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Get_page_html_api_crawl_page_html_getAsync(string url, Anonymous2? crawlRunId = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (url == null) + throw new System.ArgumentNullException("url"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/crawl/page-html" + urlBuilder_.Append("api/crawl/page-html"); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("url")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(url, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + if (crawlRunId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("crawlRunId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(crawlRunId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Pipeline Config + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Get_pipeline_config_api_pipeline_config_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/pipeline-config" + urlBuilder_.Append("api/pipeline-config"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Put Pipeline Config + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Put_pipeline_config_api_pipeline_config_putAsync(PipelineConfigBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("PUT"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/pipeline-config" + urlBuilder_.Append("api/pipeline-config"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Llm Config + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Get_llm_config_api_llm_config_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/llm-config" + urlBuilder_.Append("api/llm-config"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Put Llm Config + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Put_llm_config_api_llm_config_putAsync(LlmConfigBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("PUT"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/llm-config" + urlBuilder_.Append("api/llm-config"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Secrets + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Get_secrets_api_secrets_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/secrets" + urlBuilder_.Append("api/secrets"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Put Secrets + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Put_secrets_api_secrets_putAsync(SecretsBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("PUT"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/secrets" + urlBuilder_.Append("api/secrets"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get App Setting + /// + /// Settings key to retrieve + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Get_app_setting_api_app_settings_getAsync(string key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (key == null) + throw new System.ArgumentNullException("key"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/app-settings" + urlBuilder_.Append("api/app-settings"); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("key")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(key, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Put App Setting + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Put_app_setting_api_app_settings_putAsync(AppSettingBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("PUT"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/app-settings" + urlBuilder_.Append("api/app-settings"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// List Properties + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task List_properties_api_properties_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/properties" + urlBuilder_.Append("api/properties"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Create Property + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Create_property_api_properties_postAsync(PropertyUpsertBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/properties" + urlBuilder_.Append("api/properties"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 201) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Resolve Property + /// + /// Start URL to resolve a property from + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Resolve_property_api_properties_resolve_getAsync(string startUrl, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (startUrl == null) + throw new System.ArgumentNullException("startUrl"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/properties/resolve" + urlBuilder_.Append("api/properties/resolve"); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("startUrl")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(startUrl, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Property + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Get_property_api_properties__property_id__getAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (property_id == null) + throw new System.ArgumentNullException("property_id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/properties/{property_id}" + urlBuilder_.Append("api/properties/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(property_id, System.Globalization.CultureInfo.InvariantCulture))); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Delete Property Route + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Delete_property_route_api_properties__property_id__deleteAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (property_id == null) + throw new System.ArgumentNullException("property_id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("DELETE"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/properties/{property_id}" + urlBuilder_.Append("api/properties/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(property_id, System.Globalization.CultureInfo.InvariantCulture))); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Property Ops Route + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Get_property_ops_route_api_properties__property_id__ops_getAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (property_id == null) + throw new System.ArgumentNullException("property_id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/properties/{property_id}/ops" + urlBuilder_.Append("api/properties/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(property_id, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/ops"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Update Property Ops Route + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Update_property_ops_route_api_properties__property_id__ops_putAsync(int property_id, OpsSettingsBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (property_id == null) + throw new System.ArgumentNullException("property_id"); + + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("PUT"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/properties/{property_id}/ops" + urlBuilder_.Append("api/properties/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(property_id, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/ops"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Property Preset + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Get_property_preset_api_properties__property_id__preset_getAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (property_id == null) + throw new System.ArgumentNullException("property_id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/properties/{property_id}/preset" + urlBuilder_.Append("api/properties/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(property_id, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/preset"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Update Property Preset + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Update_property_preset_api_properties__property_id__preset_putAsync(int property_id, PresetBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (property_id == null) + throw new System.ArgumentNullException("property_id"); + + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("PUT"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/properties/{property_id}/preset" + urlBuilder_.Append("api/properties/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(property_id, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/preset"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Authorize Property Crawl Route + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Authorize_property_crawl_route_api_properties__property_id__authorize_postAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (property_id == null) + throw new System.ArgumentNullException("property_id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/properties/{property_id}/authorize" + urlBuilder_.Append("api/properties/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(property_id, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/authorize"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Property Google Status + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Property_google_status_api_properties__property_id__google_status_getAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (property_id == null) + throw new System.ArgumentNullException("property_id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/properties/{property_id}/google/status" + urlBuilder_.Append("api/properties/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(property_id, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/google/status"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Property Google Test + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Property_google_test_api_properties__property_id__google_test_postAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (property_id == null) + throw new System.ArgumentNullException("property_id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/properties/{property_id}/google/test" + urlBuilder_.Append("api/properties/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(property_id, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/google/test"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Property Google Properties + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Property_google_properties_api_properties__property_id__google_properties_getAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (property_id == null) + throw new System.ArgumentNullException("property_id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/properties/{property_id}/google/properties" + urlBuilder_.Append("api/properties/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(property_id, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/google/properties"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Property Google Links Status + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Property_google_links_status_api_properties__property_id__google_links_status_getAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (property_id == null) + throw new System.ArgumentNullException("property_id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/properties/{property_id}/google/links/status" + urlBuilder_.Append("api/properties/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(property_id, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/google/links/status"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Property Google Links Import + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Property_google_links_import_api_properties__property_id__google_links_import_postAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (property_id == null) + throw new System.ArgumentNullException("property_id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/properties/{property_id}/google/links/import" + urlBuilder_.Append("api/properties/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(property_id, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/google/links/import"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Patch Property Google Credentials + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Patch_property_google_credentials_api_properties__property_id__google_credentials_patchAsync(int property_id, GoogleCredentialsPatch body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (property_id == null) + throw new System.ArgumentNullException("property_id"); + + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("PATCH"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/properties/{property_id}/google/credentials" + urlBuilder_.Append("api/properties/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(property_id, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/google/credentials"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Post Property Google Credentials + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Post_property_google_credentials_api_properties__property_id__google_credentials_postAsync(int property_id, GoogleCredentialsPostBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (property_id == null) + throw new System.ArgumentNullException("property_id"); + + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/properties/{property_id}/google/credentials" + urlBuilder_.Append("api/properties/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(property_id, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/google/credentials"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Post Property Google Disconnect + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Post_property_google_disconnect_api_properties__property_id__google_disconnect_postAsync(int property_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (property_id == null) + throw new System.ArgumentNullException("property_id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/properties/{property_id}/google/disconnect" + urlBuilder_.Append("api/properties/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(property_id, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/google/disconnect"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// List Dashboards + /// + /// Property ID + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task List_dashboards_api_dashboards_getAsync(int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (propertyId == null) + throw new System.ArgumentNullException("propertyId"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/dashboards" + urlBuilder_.Append("api/dashboards"); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("propertyId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(propertyId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Create Dashboard + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Create_dashboard_api_dashboards_postAsync(DashboardCreateBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/dashboards" + urlBuilder_.Append("api/dashboards"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 201) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Dashboard + /// + /// Property ID + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Get_dashboard_api_dashboards__dashboard_id__getAsync(int dashboard_id, int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (dashboard_id == null) + throw new System.ArgumentNullException("dashboard_id"); + + if (propertyId == null) + throw new System.ArgumentNullException("propertyId"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/dashboards/{dashboard_id}" + urlBuilder_.Append("api/dashboards/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(dashboard_id, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("propertyId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(propertyId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Update Dashboard + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Update_dashboard_api_dashboards__dashboard_id__putAsync(int dashboard_id, DashboardUpdateBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (dashboard_id == null) + throw new System.ArgumentNullException("dashboard_id"); + + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("PUT"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/dashboards/{dashboard_id}" + urlBuilder_.Append("api/dashboards/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(dashboard_id, System.Globalization.CultureInfo.InvariantCulture))); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Delete Dashboard + /// + /// Property ID + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Delete_dashboard_api_dashboards__dashboard_id__deleteAsync(int dashboard_id, int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (dashboard_id == null) + throw new System.ArgumentNullException("dashboard_id"); + + if (propertyId == null) + throw new System.ArgumentNullException("propertyId"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("DELETE"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/dashboards/{dashboard_id}" + urlBuilder_.Append("api/dashboards/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(dashboard_id, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("propertyId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(propertyId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Dashboards Ai Generate + /// + /// + /// Generate DashScript, a widget, or a full dashboard via LLM. + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Dashboards_ai_generate_api_dashboards_ai_generate_postAsync(DashboardAiGenerateBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/dashboards/ai-generate" + urlBuilder_.Append("api/dashboards/ai-generate"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// List Filters + /// + /// Property ID + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task List_filters_api_filters_getAsync(int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (propertyId == null) + throw new System.ArgumentNullException("propertyId"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/filters" + urlBuilder_.Append("api/filters"); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("propertyId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(propertyId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Upsert Filter + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Upsert_filter_api_filters_postAsync(FilterUpsertBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/filters" + urlBuilder_.Append("api/filters"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Delete Filter + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Delete_filter_api_filters_deleteAsync(FilterDeleteBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("DELETE"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/filters" + urlBuilder_.Append("api/filters"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Google Credentials + /// + /// + /// Full app-level Google OAuth settings (server-side / local admin only). + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Get_google_credentials_api_integrations_google_credentials_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/integrations/google/credentials" + urlBuilder_.Append("api/integrations/google/credentials"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Save Google Credentials + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Save_google_credentials_api_integrations_google_credentials_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/integrations/google/credentials" + urlBuilder_.Append("api/integrations/google/credentials"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Status + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Google_status_api_integrations_google_status_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/integrations/google/status" + urlBuilder_.Append("api/integrations/google/status"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Upload Google Credentials + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Upload_google_credentials_api_integrations_google_credentials_upload_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/integrations/google/credentials/upload" + urlBuilder_.Append("api/integrations/google/credentials/upload"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Disconnect + /// + /// + /// Global disconnect is deprecated — use per-property disconnect. + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Google_disconnect_api_integrations_google_disconnect_postAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/integrations/google/disconnect" + urlBuilder_.Append("api/integrations/google/disconnect"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Oauth Start + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Google_oauth_start_api_integrations_google_auth_getAsync(Anonymous3? propertyId = null, Starturl? startUrl = null, Returnto? returnTo = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/integrations/google/auth" + urlBuilder_.Append("api/integrations/google/auth"); + urlBuilder_.Append('?'); + if (propertyId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("propertyId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(propertyId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (startUrl != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("startUrl")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(startUrl, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (returnTo != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("returnTo")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(returnTo, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Oauth Callback + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Google_oauth_callback_api_integrations_google_callback_getAsync(Code? code = null, State? state = null, Error? error = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/integrations/google/callback" + urlBuilder_.Append("api/integrations/google/callback"); + urlBuilder_.Append('?'); + if (code != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("code")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(code, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (state != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("state")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(state, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (error != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("error")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(error, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Properties Deprecated + /// + /// + /// Deprecated — use /api/properties/{id}/google/properties. + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Google_properties_deprecated_api_integrations_google_properties_getAsync(Anonymous4? propertyId = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/integrations/google/properties" + urlBuilder_.Append("api/integrations/google/properties"); + urlBuilder_.Append('?'); + if (propertyId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("propertyId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(propertyId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Test + /// + /// + /// Run `python -m src google --test` and return stdout log. + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Google_test_api_integrations_google_test_postAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/integrations/google/test" + urlBuilder_.Append("api/integrations/google/test"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Page Data + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Google_page_data_api_integrations_google_page_data_getAsync(string url, Googlesnapshotid? googleSnapshotId = null, Anonymous5? propertyId = null, Anonymous6? domain = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (url == null) + throw new System.ArgumentNullException("url"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/integrations/google/page-data" + urlBuilder_.Append("api/integrations/google/page-data"); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("url")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(url, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + if (googleSnapshotId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("googleSnapshotId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(googleSnapshotId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (propertyId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("propertyId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(propertyId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (domain != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("domain")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(domain, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Page Data History + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Google_page_data_history_api_integrations_google_page_data_history_getAsync(string url, Anonymous7? propertyId = null, Anonymous8? domain = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (url == null) + throw new System.ArgumentNullException("url"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/integrations/google/page-data/history" + urlBuilder_.Append("api/integrations/google/page-data/history"); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("url")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(url, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + if (propertyId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("propertyId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(propertyId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (domain != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("domain")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(domain, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Page Live + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Google_page_live_api_integrations_google_page_live_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/integrations/google/page-live" + urlBuilder_.Append("api/integrations/google/page-live"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Keywords By Page + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Google_keywords_by_page_api_integrations_google_keywords_by_page_getAsync(string url, Anonymous9? propertyId = null, Anonymous10? domain = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (url == null) + throw new System.ArgumentNullException("url"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/integrations/google/keywords/by-page" + urlBuilder_.Append("api/integrations/google/keywords/by-page"); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("url")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(url, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + if (propertyId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("propertyId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(propertyId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (domain != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("domain")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(domain, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Keywords History + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Google_keywords_history_api_integrations_google_keywords_history_getAsync(string keyword, Anonymous11? propertyId = null, Anonymous12? domain = null, int? limit = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (keyword == null) + throw new System.ArgumentNullException("keyword"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/integrations/google/keywords/history" + urlBuilder_.Append("api/integrations/google/keywords/history"); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("keyword")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(keyword, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + if (propertyId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("propertyId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(propertyId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (domain != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("domain")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(domain, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (limit != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("limit")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(limit, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Bing Sync + /// + /// + /// Fetch Bing Webmaster backlinks summary using config from DB. + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Bing_sync_api_integrations_bing_sync_postAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/integrations/bing/sync" + urlBuilder_.Append("api/integrations/bing/sync"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Page Compare + /// + /// + /// Compare two page Google data snapshots. + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Google_page_compare_api_integrations_google_page_compare_getAsync(string url, int currentId, int baselineId, string? currentType = null, string? baselineType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (url == null) + throw new System.ArgumentNullException("url"); + + if (currentId == null) + throw new System.ArgumentNullException("currentId"); + + if (baselineId == null) + throw new System.ArgumentNullException("baselineId"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/integrations/google/page-compare" + urlBuilder_.Append("api/integrations/google/page-compare"); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("url")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(url, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + urlBuilder_.Append(System.Uri.EscapeDataString("currentId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(currentId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + urlBuilder_.Append(System.Uri.EscapeDataString("baselineId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(baselineId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + if (currentType != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("currentType")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(currentType, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (baselineType != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("baselineType")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(baselineType, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Page Live History + /// + /// + /// Return history of page Google snapshots for a URL. + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Google_page_live_history_api_integrations_google_page_live_history_getAsync(string url, int? limit = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (url == null) + throw new System.ArgumentNullException("url"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/integrations/google/page-live/history" + urlBuilder_.Append("api/integrations/google/page-live/history"); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("url")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(url, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + if (limit != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("limit")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(limit, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Keywords History Batch + /// + /// + /// Batch keyword history: { keywords: str[], limit?: int, propertyId?: int, domain?: str } + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Google_keywords_history_batch_api_integrations_google_keywords_history_batch_postAsync(object body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/integrations/google/keywords/history/batch" + urlBuilder_.Append("api/integrations/google/keywords/history/batch"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Keywords Expand + /// + /// + /// Expand keyword ideas from Google Keyword Planner or suggest API. + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Google_keywords_expand_api_integrations_google_keywords_expand_postAsync(object body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/integrations/google/keywords/expand" + urlBuilder_.Append("api/integrations/google/keywords/expand"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Google Keywords Planner + /// + /// + /// Fetch keyword planner data from Google Ads API. + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Google_keywords_planner_api_integrations_google_keywords_planner_postAsync(object body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/integrations/google/keywords/planner" + urlBuilder_.Append("api/integrations/google/keywords/planner"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// List Issue Status Route + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task List_issue_status_route_api_issues_status_getAsync(int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (propertyId == null) + throw new System.ArgumentNullException("propertyId"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/issues/status" + urlBuilder_.Append("api/issues/status"); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("propertyId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(propertyId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Upsert Issue Status Route + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Upsert_issue_status_route_api_issues_status_putAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("PUT"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/issues/status" + urlBuilder_.Append("api/issues/status"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Issues Fix Suggestion + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Issues_fix_suggestion_api_issues_fix_suggestion_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/issues/fix-suggestion" + urlBuilder_.Append("api/issues/fix-suggestion"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Issues Action Plan + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Issues_action_plan_api_issues_action_plan_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/issues/action-plan" + urlBuilder_.Append("api/issues/action-plan"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Ai Fix Suggestion + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Ai_fix_suggestion_api_ai_fix_suggestion_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/ai/fix-suggestion" + urlBuilder_.Append("api/ai/fix-suggestion"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Keywords Competitor Import + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Keywords_competitor_import_api_keywords_competitor_import_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/keywords/competitor-import" + urlBuilder_.Append("api/keywords/competitor-import"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Keywords Content Brief + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Keywords_content_brief_api_keywords_content_brief_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/keywords/content-brief" + urlBuilder_.Append("api/keywords/content-brief"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Backlinks Velocity + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Backlinks_velocity_api_backlinks_velocity_getAsync(int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (propertyId == null) + throw new System.ArgumentNullException("propertyId"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/backlinks/velocity" + urlBuilder_.Append("api/backlinks/velocity"); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("propertyId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(propertyId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Backlinks Competitor Import + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Backlinks_competitor_import_api_backlinks_competitor_import_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/backlinks/competitor-import" + urlBuilder_.Append("api/backlinks/competitor-import"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Backlinks Third Party Import + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Backlinks_third_party_import_api_backlinks_third_party_import_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/backlinks/third-party-import" + urlBuilder_.Append("api/backlinks/third-party-import"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Content Analyze + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Content_analyze_api_content_analyze_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/content/analyze" + urlBuilder_.Append("api/content/analyze"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Content Score + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Content_score_api_content_score_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/content/score" + urlBuilder_.Append("api/content/score"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Content Wizard + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Content_wizard_api_content_wizard_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/content/wizard" + urlBuilder_.Append("api/content/wizard"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// List Content Drafts Route + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task List_content_drafts_route_api_content_drafts_getAsync(int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (propertyId == null) + throw new System.ArgumentNullException("propertyId"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/content-drafts" + urlBuilder_.Append("api/content-drafts"); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("propertyId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(propertyId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Create Content Draft Route + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Create_content_draft_route_api_content_drafts_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/content-drafts" + urlBuilder_.Append("api/content-drafts"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get Content Draft Route + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Get_content_draft_route_api_content_drafts__draft_id__getAsync(int draft_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (draft_id == null) + throw new System.ArgumentNullException("draft_id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/content-drafts/{draft_id}" + urlBuilder_.Append("api/content-drafts/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(draft_id, System.Globalization.CultureInfo.InvariantCulture))); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Update Content Draft Route + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Update_content_draft_route_api_content_drafts__draft_id__patchAsync(int draft_id, object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (draft_id == null) + throw new System.ArgumentNullException("draft_id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("PATCH"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/content-drafts/{draft_id}" + urlBuilder_.Append("api/content-drafts/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(draft_id, System.Globalization.CultureInfo.InvariantCulture))); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Delete Content Draft Route + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Delete_content_draft_route_api_content_drafts__draft_id__deleteAsync(int draft_id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (draft_id == null) + throw new System.ArgumentNullException("draft_id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("DELETE"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/content-drafts/{draft_id}" + urlBuilder_.Append("api/content-drafts/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(draft_id, System.Globalization.CultureInfo.InvariantCulture))); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// List Page Markdown Route + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task List_page_markdown_route_api_page_markdown_getAsync(int crawlRunId, int? page = null, int? limit = null, Q? q = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (crawlRunId == null) + throw new System.ArgumentNullException("crawlRunId"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/page-markdown" + urlBuilder_.Append("api/page-markdown"); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("crawlRunId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(crawlRunId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + if (page != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("page")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(page, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (limit != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("limit")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(limit, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (q != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("q")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(q, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Delete Page Markdown Route + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Delete_page_markdown_route_api_page_markdown_deleteAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("DELETE"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/page-markdown" + urlBuilder_.Append("api/page-markdown"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Page Markdown Content Route + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Page_markdown_content_route_api_page_markdown_content_getAsync(int crawlRunId, string url, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (crawlRunId == null) + throw new System.ArgumentNullException("crawlRunId"); + + if (url == null) + throw new System.ArgumentNullException("url"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/page-markdown/content" + urlBuilder_.Append("api/page-markdown/content"); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("crawlRunId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(crawlRunId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + urlBuilder_.Append(System.Uri.EscapeDataString("url")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(url, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Page Markdown Extract + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Page_markdown_extract_api_page_markdown_extract_postAsync(object? body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/page-markdown/extract" + urlBuilder_.Append("api/page-markdown/extract"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Page Markdown Runs Route + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Page_markdown_runs_route_api_page_markdown_runs_getAsync(Anonymous13? propertyId = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/page-markdown/runs" + urlBuilder_.Append("api/page-markdown/runs"); + urlBuilder_.Append('?'); + if (propertyId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("propertyId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(propertyId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Ollama Status + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Ollama_status_api_ollama_status_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/ollama/status" + urlBuilder_.Append("api/ollama/status"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Mcp Tools + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Mcp_tools_api_mcp_tools_getAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/mcp-tools" + urlBuilder_.Append("api/mcp-tools"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Delete Portfolio Item + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Delete_portfolio_item_api_portfolio_delete_deleteAsync(DeletePortfolioBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("DELETE"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/portfolio/delete" + urlBuilder_.Append("api/portfolio/delete"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Alerts Check + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Alerts_check_api_alerts_check_postAsync(int propertyId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (propertyId == null) + throw new System.ArgumentNullException("propertyId"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/alerts/check" + urlBuilder_.Append("api/alerts/check"); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("propertyId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(propertyId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Schedule Check + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Schedule_check_api_schedule_check_postAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/schedule/check" + urlBuilder_.Append("api/schedule/check"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Logs Upload + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Logs_upload_api_logs_upload_postAsync(int? propertyId = null, string? file = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var boundary_ = System.Guid.NewGuid().ToString(); + var content_ = new System.Net.Http.MultipartFormDataContent(boundary_); + content_.Headers.Remove("Content-Type"); + content_.Headers.TryAddWithoutValidation("Content-Type", "multipart/form-data; boundary=" + boundary_); + + if (propertyId == null) + throw new System.ArgumentNullException("propertyId"); + else + { + content_.Add(new System.Net.Http.StringContent(ConvertToString(propertyId, System.Globalization.CultureInfo.InvariantCulture)), "propertyId"); + } + + if (file == null) + throw new System.ArgumentNullException("file"); + else + { + content_.Add(new System.Net.Http.StringContent(ConvertToString(file, System.Globalization.CultureInfo.InvariantCulture)), "file"); + } + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/logs/upload" + urlBuilder_.Append("api/logs/upload"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Compare Export + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Compare_export_api_compare_export_postAsync(CompareExportBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/compare/export" + urlBuilder_.Append("api/compare/export"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Page Coach + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Page_coach_api_links_page_coach_postAsync(PageCoachBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/links/page-coach" + urlBuilder_.Append("api/links/page-coach"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Run Audit Tool + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Run_audit_tool_api_report_audit_tool_postAsync(AuditToolBody body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/report/audit-tool" + urlBuilder_.Append("api/report/audit-tool"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Export Report + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Export_report_api_report_export_getAsync(string? format = null, Anonymous14? reportId = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/report/export" + urlBuilder_.Append("api/report/export"); + urlBuilder_.Append('?'); + if (format != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("format")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(format, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (reportId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("reportId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(reportId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Export Sitemap + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Export_sitemap_api_report_export_sitemap_getAsync(Anonymous15? reportId = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/report/export-sitemap" + urlBuilder_.Append("api/report/export-sitemap"); + urlBuilder_.Append('?'); + if (reportId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("reportId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(reportId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Report Portfolio + /// + /// + /// Return portfolio data — groups, crawl history, summary, or single card. + /// + /// Successful Response + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task Report_portfolio_api_report_portfolio_getAsync(string? widget = null, Ids? ids = null, Anonymous16? reportId = null, Anonymous17? crawlRunId = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/report/portfolio" + urlBuilder_.Append("api/report/portfolio"); + urlBuilder_.Append('?'); + if (widget != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("widget")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(widget, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (ids != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("ids")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(ids, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (reportId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("reportId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(reportId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (crawlRunId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("crawlRunId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(crawlRunId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 422) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new FastApiClientException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new FastApiClientException("Validation Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false); + throw new FastApiClientException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + protected struct ObjectResponseResult + { + public ObjectResponseResult(T responseObject, string responseText) + { + this.Object = responseObject; + this.Text = responseText; + } + + public T Object { get; } + + public string Text { get; } + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + private static System.Threading.Tasks.Task ReadAsStringAsync(System.Net.Http.HttpContent content, System.Threading.CancellationToken cancellationToken) + { + #if NET5_0_OR_GREATER + return content.ReadAsStringAsync(cancellationToken); + #else + return content.ReadAsStringAsync(); + #endif + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + private static System.Threading.Tasks.Task ReadAsStreamAsync(System.Net.Http.HttpContent content, System.Threading.CancellationToken cancellationToken) + { + #if NET5_0_OR_GREATER + return content.ReadAsStreamAsync(cancellationToken); + #else + return content.ReadAsStreamAsync(); + #endif + } + + public bool ReadResponseAsString { get; set; } + + protected virtual async System.Threading.Tasks.Task> ReadObjectResponseAsync(System.Net.Http.HttpResponseMessage response, System.Collections.Generic.IReadOnlyDictionary> headers, System.Threading.CancellationToken cancellationToken) + { + if (response == null || response.Content == null) + { + return new ObjectResponseResult(default(T)!, string.Empty); + } + + if (ReadResponseAsString) + { + var responseText = await ReadAsStringAsync(response.Content, cancellationToken).ConfigureAwait(false); + try + { + var typedBody = System.Text.Json.JsonSerializer.Deserialize(responseText, JsonSerializerSettings); + return new ObjectResponseResult(typedBody!, responseText); + } + catch (System.Text.Json.JsonException exception) + { + var message = "Could not deserialize the response body string as " + typeof(T).FullName + "."; + throw new FastApiClientException(message, (int)response.StatusCode, responseText, headers, exception); + } + } + else + { + try + { + using (var responseStream = await ReadAsStreamAsync(response.Content, cancellationToken).ConfigureAwait(false)) + { + var typedBody = await System.Text.Json.JsonSerializer.DeserializeAsync(responseStream, JsonSerializerSettings, cancellationToken).ConfigureAwait(false); + return new ObjectResponseResult(typedBody!, string.Empty); + } + } + catch (System.Text.Json.JsonException exception) + { + var message = "Could not deserialize the response body stream as " + typeof(T).FullName + "."; + throw new FastApiClientException(message, (int)response.StatusCode, string.Empty, headers, exception); + } + } + } + + private string ConvertToString(object? value, System.Globalization.CultureInfo cultureInfo) + { + if (value == null) + { + return ""; + } + + if (value is System.Enum) + { + var name = System.Enum.GetName(value.GetType(), value); + if (name != null) + { + var field_ = System.Reflection.IntrospectionExtensions.GetTypeInfo(value.GetType()).GetDeclaredField(name); + if (field_ != null) + { + var attribute = System.Reflection.CustomAttributeExtensions.GetCustomAttribute(field_, typeof(System.Runtime.Serialization.EnumMemberAttribute)) + as System.Runtime.Serialization.EnumMemberAttribute; + if (attribute != null) + { + return attribute.Value != null ? attribute.Value : name; + } + } + + var converted = System.Convert.ToString(System.Convert.ChangeType(value, System.Enum.GetUnderlyingType(value.GetType()), cultureInfo)); + return converted == null ? string.Empty : converted; + } + } + else if (value is bool) + { + return System.Convert.ToString((bool)value, cultureInfo).ToLowerInvariant(); + } + else if (value is byte[]) + { + return System.Convert.ToBase64String((byte[]) value); + } + else if (value is string[]) + { + return string.Join(",", (string[])value); + } + else if (value.GetType().IsArray) + { + var valueArray = (System.Array)value; + var valueTextArray = new string[valueArray.Length]; + for (var i = 0; i < valueArray.Length; i++) + { + valueTextArray[i] = ConvertToString(valueArray.GetValue(i), cultureInfo); + } + return string.Join(",", valueTextArray); + } + + var result = System.Convert.ToString(value, cultureInfo); + return result == null ? "" : result; + } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class AppSettingBody + { + + [System.Text.Json.Serialization.JsonPropertyName("key")] + public string Key { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("value")] + public string Value { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class AuditToolBody + { + + [System.Text.Json.Serialization.JsonPropertyName("toolName")] + public string ToolName { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("propertyId")] + public int PropertyId { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("reportId")] + public ReportId ReportId { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("args")] + public object Args { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Body_logs_upload_api_logs_upload_post + { + + [System.Text.Json.Serialization.JsonPropertyName("propertyId")] + public int PropertyId { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("file")] + public string File { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class CancelResponse + { + + [System.Text.Json.Serialization.JsonPropertyName("ok")] + public bool Ok { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("status")] + public string Status { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("error")] + public Error2 Error { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ChatRequest + { + + [System.Text.Json.Serialization.JsonPropertyName("sessionId")] + public int SessionId { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("propertyId")] + public int PropertyId { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("message")] + public string Message { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("reportId")] + public ReportId2 ReportId { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ChatSessionCreate + { + + [System.Text.Json.Serialization.JsonPropertyName("propertyId")] + public int PropertyId { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("title")] + public string Title { get; set; } = "New chat"; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class CompareExportBody + { + + [System.Text.Json.Serialization.JsonPropertyName("reportIdA")] + public ReportIdA ReportIdA { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("reportIdB")] + public ReportIdB ReportIdB { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class DashboardAiGenerateBody + { + + [System.Text.Json.Serialization.JsonPropertyName("mode")] + public string Mode { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("prompt")] + public string Prompt { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("catalog")] + public System.Collections.Generic.List Catalog { get; set; } = new System.Collections.Generic.List(); + + [System.Text.Json.Serialization.JsonPropertyName("viz_types")] + public System.Collections.Generic.Dictionary Viz_types { get; set; } = new System.Collections.Generic.Dictionary(); + + [System.Text.Json.Serialization.JsonPropertyName("dashscript_help")] + public string Dashscript_help { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("toolName")] + public ToolName ToolName { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("propertyId")] + public PropertyId PropertyId { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("reportId")] + public ReportId3 ReportId { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("current")] + public Current Current { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("sample")] + public Sample Sample { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class DashboardCreateBody + { + + [System.Text.Json.Serialization.JsonPropertyName("propertyId")] + public int PropertyId { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("name")] + public Name Name { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("layoutJson")] + public LayoutJson LayoutJson { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class DashboardUpdateBody + { + + [System.Text.Json.Serialization.JsonPropertyName("propertyId")] + public int PropertyId { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("name")] + public Name2 Name { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("layoutJson")] + public LayoutJson2 LayoutJson { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("isDefault")] + public IsDefault IsDefault { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class DeletePortfolioBody + { + + [System.Text.Json.Serialization.JsonPropertyName("reportId")] + public ReportId4 ReportId { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("crawlRunId")] + public CrawlRunId CrawlRunId { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class FilterDeleteBody + { + + [System.Text.Json.Serialization.JsonPropertyName("propertyId")] + public int PropertyId { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string Name { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class FilterUpsertBody + { + + [System.Text.Json.Serialization.JsonPropertyName("propertyId")] + public int PropertyId { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string Name { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("filterJson")] + public FilterJson FilterJson { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class GoogleCredentialsPatch + { + + [System.Text.Json.Serialization.JsonPropertyName("refreshToken")] + public RefreshToken RefreshToken { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("authMode")] + public AuthMode AuthMode { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("gscSiteUrl")] + public GscSiteUrl GscSiteUrl { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("ga4PropertyId")] + public Ga4PropertyId Ga4PropertyId { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("dateRangeDays")] + public DateRangeDays DateRangeDays { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("connectedEmail")] + public ConnectedEmail ConnectedEmail { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class GoogleCredentialsPostBody + { + + [System.Text.Json.Serialization.JsonPropertyName("gscSiteUrl")] + public GscSiteUrl2 GscSiteUrl { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("ga4PropertyId")] + public Ga4PropertyId2 Ga4PropertyId { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("dateRangeDays")] + public DateRangeDays2 DateRangeDays { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("refreshToken")] + public RefreshToken2 RefreshToken { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class HTTPValidationError + { + + [System.Text.Json.Serialization.JsonPropertyName("detail")] + public System.Collections.Generic.List Detail { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class JobsListResponse + { + + [System.Text.Json.Serialization.JsonPropertyName("jobs")] + public System.Collections.Generic.List Jobs { get; set; } = new System.Collections.Generic.List(); + + [System.Text.Json.Serialization.JsonPropertyName("active")] + public Active Active { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("reconciled")] + public int Reconciled { get; set; } = 0; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class LlmConfigBody + { + + [System.Text.Json.Serialization.JsonPropertyName("state")] + public object State { get; set; } = new object(); + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class OpsSettingsBody + { + + [System.Text.Json.Serialization.JsonPropertyName("scheduleCron")] + public ScheduleCron ScheduleCron { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("alertWebhookUrl")] + public AlertWebhookUrl AlertWebhookUrl { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("alertEmail")] + public AlertEmail AlertEmail { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class PageCoachBody + { + + [System.Text.Json.Serialization.JsonPropertyName("url")] + public Url Url { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("refresh")] + public bool Refresh { get; set; } = false; + + [System.Text.Json.Serialization.JsonPropertyName("currentType")] + public CurrentType CurrentType { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("currentId")] + public CurrentId CurrentId { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("baselineType")] + public BaselineType BaselineType { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("baselineId")] + public BaselineId BaselineId { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("propertyId")] + public PropertyId2 PropertyId { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class PauseResponse + { + + [System.Text.Json.Serialization.JsonPropertyName("ok")] + public bool Ok { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("error")] + public Error3 Error { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class PipelineConfigBody + { + + [System.Text.Json.Serialization.JsonPropertyName("state")] + public object State { get; set; } = new object(); + + [System.Text.Json.Serialization.JsonPropertyName("unknownKeys")] + public UnknownKeys UnknownKeys { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class PresetBody + { + + [System.Text.Json.Serialization.JsonPropertyName("preset")] + public Preset Preset { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class PropertyUpsertBody + { + + [System.Text.Json.Serialization.JsonPropertyName("name")] + public Name3 Name { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("canonical_domain")] + public Canonical_domain Canonical_domain { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("site_url")] + public Site_url Site_url { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ResumeResponse + { + + [System.Text.Json.Serialization.JsonPropertyName("ok")] + public bool Ok { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("newJobId")] + public NewJobId NewJobId { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("error")] + public Error4 Error { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class RunPostBody + { + + [System.Text.Json.Serialization.JsonPropertyName("command")] + public Command Command { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("state")] + public State2 State { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("unknownKeys")] + public System.Collections.Generic.List UnknownKeys { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("llmState")] + public LlmState LlmState { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("propertyId")] + public PropertyId3 PropertyId { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("python")] + public Python Python { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("repoRoot")] + public RepoRoot RepoRoot { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class RunResponse + { + + [System.Text.Json.Serialization.JsonPropertyName("jobId")] + public string JobId { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class SecretsBody + { + + [System.Text.Json.Serialization.JsonPropertyName("state")] + public object State { get; set; } = new object(); + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class UnknownKeyEntry + { + + [System.Text.Json.Serialization.JsonPropertyName("key")] + public string Key { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("value")] + public string Value { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ValidationError + { + + [System.Text.Json.Serialization.JsonPropertyName("loc")] + public System.Collections.Generic.List Loc { get; set; } = new System.Collections.Generic.List(); + + [System.Text.Json.Serialization.JsonPropertyName("msg")] + public string Msg { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("type")] + public string Type { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("input")] + public object Input { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("ctx")] + public object Ctx { get; set; } = default!; + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Reportid + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Domain + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Section + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Propertyid + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Anonymous + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Crawlrunid + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Id + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + /// + /// Crawl run ID + /// + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Anonymous2 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Anonymous3 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Starturl + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Returnto + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Code + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class State + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Error + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Anonymous4 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Googlesnapshotid + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Anonymous5 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Anonymous6 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Anonymous7 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Anonymous8 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Anonymous9 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Anonymous10 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Anonymous11 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Anonymous12 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Q + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Anonymous13 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Anonymous14 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Anonymous15 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Ids + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Anonymous16 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Anonymous17 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ReportId + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Error2 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ReportId2 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ReportIdA + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ReportIdB + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ToolName + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class PropertyId + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ReportId3 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Current + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Sample + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Name + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class LayoutJson + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Name2 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class LayoutJson2 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class IsDefault + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ReportId4 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class CrawlRunId + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class FilterJson + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class RefreshToken + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class AuthMode + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class GscSiteUrl + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Ga4PropertyId + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class DateRangeDays + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ConnectedEmail + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class GscSiteUrl2 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Ga4PropertyId2 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class DateRangeDays2 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class RefreshToken2 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Active + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ScheduleCron + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class AlertWebhookUrl + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class AlertEmail + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Url + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class CurrentType + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class CurrentId + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class BaselineType + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class BaselineId + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class PropertyId2 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Error3 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class UnknownKeys + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Preset + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Name3 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Canonical_domain + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Site_url + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class NewJobId + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Error4 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Command + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class State2 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class LlmState + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class PropertyId3 + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Python + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class RepoRoot + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Loc + { + + private System.Collections.Generic.IDictionary? _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class FastApiClientException : System.Exception + { + public int StatusCode { get; private set; } + + public string? Response { get; private set; } + + public System.Collections.Generic.IReadOnlyDictionary> Headers { get; private set; } + + public FastApiClientException(string message, int statusCode, string? response, System.Collections.Generic.IReadOnlyDictionary> headers, System.Exception? innerException) + : base(message + "\n\nStatus: " + statusCode + "\nResponse: \n" + ((response == null) ? "(null)" : response.Substring(0, response.Length >= 512 ? 512 : response.Length)), innerException) + { + StatusCode = statusCode; + Response = response; + Headers = headers; + } + + public override string ToString() + { + return string.Format("HTTP Response: \n\n{0}\n\n{1}", Response, base.ToString()); + } + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class FastApiClientException : FastApiClientException + { + public TResult Result { get; private set; } + + public FastApiClientException(string message, int statusCode, string? response, System.Collections.Generic.IReadOnlyDictionary> headers, TResult result, System.Exception? innerException) + : base(message, statusCode, response, headers, innerException) + { + Result = result; + } + } + +} + +#pragma warning restore 108 +#pragma warning restore 114 +#pragma warning restore 472 +#pragma warning restore 612 +#pragma warning restore 649 +#pragma warning restore 1573 +#pragma warning restore 1591 +#pragma warning restore 8073 +#pragma warning restore 3016 +#pragma warning restore 8600 +#pragma warning restore 8602 +#pragma warning restore 8603 +#pragma warning restore 8604 +#pragma warning restore 8625 +#pragma warning restore 8765 \ No newline at end of file diff --git a/services/Bff/src/Bff.Application/Http/IdempotentRetryHandler.cs b/services/Bff/src/Bff.Application/Http/IdempotentRetryHandler.cs new file mode 100644 index 00000000..3f42eee0 --- /dev/null +++ b/services/Bff/src/Bff.Application/Http/IdempotentRetryHandler.cs @@ -0,0 +1,51 @@ +using System.Net; + +namespace Bff.Application.Http; + +/// +/// Minimal, dependency-free resilience: retries transient failures for idempotent +/// methods only (GET/HEAD) — never POST/PUT/PATCH/DELETE, so mutations can't double-submit. +/// This is the right-sized stand-in for a full Polly pipeline in a single-deployment app +/// (no circuit breaker by design). Attached only to the non-streaming FastAPI client. +/// +public sealed class IdempotentRetryHandler : DelegatingHandler +{ + private const int MaxRetries = 2; + private static readonly TimeSpan BaseDelay = TimeSpan.FromMilliseconds(150); + + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + var idempotent = request.Method == HttpMethod.Get || request.Method == HttpMethod.Head; + if (!idempotent) + { + return await base.SendAsync(request, cancellationToken); + } + + for (var attempt = 0; ; attempt++) + { + try + { + var response = await base.SendAsync(request, cancellationToken); + if (attempt >= MaxRetries || !IsTransient(response.StatusCode)) + { + return response; + } + response.Dispose(); + } + catch (HttpRequestException) when (attempt < MaxRetries) + { + // fall through to retry + } + + await Task.Delay(BaseDelay * (attempt + 1), cancellationToken); + } + } + + private static bool IsTransient(HttpStatusCode status) => + status == HttpStatusCode.RequestTimeout // 408 + || status == HttpStatusCode.BadGateway // 502 + || status == HttpStatusCode.ServiceUnavailable // 503 + || status == HttpStatusCode.GatewayTimeout; // 504 +} diff --git a/services/Bff/src/Bff.Application/Options/AuthOptions.cs b/services/Bff/src/Bff.Application/Options/AuthOptions.cs new file mode 100644 index 00000000..79ae77f6 --- /dev/null +++ b/services/Bff/src/Bff.Application/Options/AuthOptions.cs @@ -0,0 +1,41 @@ +namespace Bff.Application.Options; + +/// +/// Auth configuration mirroring the TS env contract (web/src/server/auth.ts): +/// AUTH_SECRET / SESSION_SECRET, AUTH_USER, AUTH_PASSWORD, AUTH_DEFAULT_ROLE. +/// When is empty, auth is disabled (everything is permitted), +/// matching TS authEnabled() === false. +/// +public sealed class AuthOptions +{ + public const string SectionName = "Auth"; + + /// HMAC signing secret. Empty = auth disabled. + public string Secret { get; set; } = string.Empty; + + /// Basic-auth username for login (TS AUTH_USER, default "admin"). + public string BasicUser { get; set; } = "admin"; + + /// Basic-auth password for login (TS AUTH_PASSWORD). Empty = basic login unavailable. + public string BasicPassword { get; set; } = string.Empty; + + /// Role granted on successful login (TS AUTH_DEFAULT_ROLE, default "analyst"). + public string DefaultRole { get; set; } = "analyst"; + + /// Session lifetime in seconds (TS SESSION_MAX_AGE_S = 7 days). + public int SessionMaxAgeSeconds { get; set; } = 60 * 60 * 24 * 7; + + /// + /// Cookie SameSite mode for the wp_session cookie. Dev (same-site localhost): "Lax". + /// Cross-site prod (frontend + BFF on a shared parent domain over HTTPS): "None". + /// + public string CookieSameSite { get; set; } = "Lax"; + + /// Set the Secure flag on the cookie. Required when CookieSameSite = None. + public bool CookieSecure { get; set; } + + /// Optional cookie Domain (e.g. ".example.com") for cross-subdomain prod. Empty = host-only. + public string CookieDomain { get; set; } = string.Empty; + + public bool Enabled => !string.IsNullOrEmpty(Secret); +} diff --git a/services/Bff/src/Bff.Application/Options/BffCorsOptions.cs b/services/Bff/src/Bff.Application/Options/BffCorsOptions.cs new file mode 100644 index 00000000..3de42727 --- /dev/null +++ b/services/Bff/src/Bff.Application/Options/BffCorsOptions.cs @@ -0,0 +1,13 @@ +namespace Bff.Application.Options; + +/// +/// Credentialed-CORS configuration. The browser calls the BFF cross-origin, so we must +/// echo an explicit allow-list of origins (never a wildcard with credentials). +/// Env override: BFF_ALLOWED_ORIGINS (comma-separated), mirroring FASTAPI_ALLOWED_ORIGINS. +/// +public sealed class BffCorsOptions +{ + public const string SectionName = "Cors"; + + public string[] AllowedOrigins { get; set; } = []; +} diff --git a/services/Bff/src/Bff.Application/Options/UpstreamOptions.cs b/services/Bff/src/Bff.Application/Options/UpstreamOptions.cs new file mode 100644 index 00000000..b53259b2 --- /dev/null +++ b/services/Bff/src/Bff.Application/Options/UpstreamOptions.cs @@ -0,0 +1,29 @@ +namespace Bff.Application.Options; + +/// +/// Base URLs + timeouts for the services the BFF fronts. Both upstreams are +/// network-internal; only the BFF is browser-facing. +/// +public sealed class UpstreamOptions +{ + public const string SectionName = "Upstream"; + + /// FastAPI base URL (env override: FASTAPI_URL). Default matches the new internal compose service. + public string FastApiBaseUrl { get; set; } = "http://127.0.0.1:8001"; + + /// FileService base URL (env override: FILE_SERVICE_URL). + public string FileServiceBaseUrl { get; set; } = "http://127.0.0.1:8080"; + + /// Timeout for normal (non-streaming) upstream calls. Parity with the TS proxy (120s). + public int TimeoutSeconds { get; set; } = 120; + + /// Data service base URL (env override: DATA_SERVICE_URL). Internal .NET read service. + public string DataBaseUrl { get; set; } = "http://127.0.0.1:8091"; + + /// + /// Comma-separated /api path prefixes routed to the Data service instead of FastAPI + /// (env override: DATA_ROUTES). Supports GET/HEAD reads and POST/PUT/DELETE mutations on matched + /// prefixes. Empty = everything stays on FastAPI (rollback-safe default). + /// + public string[] DataRoutes { get; set; } = []; +} diff --git a/services/Bff/src/Bff.Domain/Bff.Domain.csproj b/services/Bff/src/Bff.Domain/Bff.Domain.csproj new file mode 100644 index 00000000..9ed914b5 --- /dev/null +++ b/services/Bff/src/Bff.Domain/Bff.Domain.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/services/Bff/src/Bff.Domain/Roles.cs b/services/Bff/src/Bff.Domain/Roles.cs new file mode 100644 index 00000000..1f0ee891 --- /dev/null +++ b/services/Bff/src/Bff.Domain/Roles.cs @@ -0,0 +1,45 @@ +namespace Bff.Domain; + +/// +/// Role model, kept byte-compatible with the TypeScript auth layer +/// (web/src/server/auth.ts). The BFF is the single browser-facing API surface and +/// terminates auth, so these tiers must mirror the Next.js semantics exactly. +/// +public static class Roles +{ + public const string Admin = "admin"; + public const string Editor = "editor"; + public const string Analyst = "analyst"; + public const string Viewer = "viewer"; + public const string ClientReadonly = "client-readonly"; + + /// Read-only roles (TS: READONLY_ROLES). Cannot mutate. + private static readonly HashSet ReadonlyRoles = new(StringComparer.Ordinal) + { + Viewer, + ClientReadonly, + }; + + /// TS canMutateRole: admin/editor/analyst may mutate; viewer/client-readonly may not. + public static bool CanMutate(string? role) + { + if (string.IsNullOrEmpty(role)) + { + return false; + } + return !ReadonlyRoles.Contains(role); + } + + /// TS requireApiAuthForChat: chat allows client-readonly but blocks viewer. + public static bool CanChat(string? role) + { + if (string.IsNullOrEmpty(role)) + { + return false; + } + return role != Viewer; + } + + public static bool IsReadonly(string? role) => + !string.IsNullOrEmpty(role) && ReadonlyRoles.Contains(role); +} diff --git a/services/Bff/tests/Bff.Tests/Bff.Tests.csproj b/services/Bff/tests/Bff.Tests/Bff.Tests.csproj new file mode 100644 index 00000000..b119c560 --- /dev/null +++ b/services/Bff/tests/Bff.Tests/Bff.Tests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + diff --git a/services/Bff/tests/Bff.Tests/GatewayTests.cs b/services/Bff/tests/Bff.Tests/GatewayTests.cs new file mode 100644 index 00000000..38f5e1fa --- /dev/null +++ b/services/Bff/tests/Bff.Tests/GatewayTests.cs @@ -0,0 +1,453 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; +using Bff.Application; +using Bff.Application.Auth; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Bff.Tests; + +public class GatewayTests +{ + private const string Secret = "test-secret-123"; + + [Fact] + public async Task Health_returns_ok_without_auth() + { + using var factory = new BffFactory(); + var client = factory.CreateClient(); + var response = await client.GetAsync("/health"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task Passthrough_forwards_to_fastapi_and_returns_body() + { + using var factory = new BffFactory(); + var client = factory.CreateClient(); + var response = await client.GetAsync("/api/report/meta?x=1"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + Assert.Contains("/api/report/meta", body); // upstream echoes the forwarded path + } + + [Fact] + public async Task Read_requires_authentication_when_auth_enabled() + { + using var factory = new BffFactory(secret: Secret); + var client = factory.CreateClient(); + var response = await client.GetAsync("/api/report/meta"); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Equal("application/problem+json", response.Content.Headers.ContentType?.MediaType); + } + + [Fact] + public async Task Read_allowed_for_readonly_role() + { + using var factory = new BffFactory(secret: Secret); + var response = await Send(factory, HttpMethod.Get, "/api/report/meta", "viewer"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task Mutate_requires_authentication() + { + using var factory = new BffFactory(secret: Secret); + var client = factory.CreateClient(); + var response = await client.PostAsync("/api/run", null); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact] + public async Task Mutate_forbidden_for_readonly_role() + { + using var factory = new BffFactory(secret: Secret); + var response = await Send(factory, HttpMethod.Post, "/api/run", "client-readonly"); + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task Mutate_allowed_for_analyst_role() + { + using var factory = new BffFactory(secret: Secret); + var response = await Send(factory, HttpMethod.Post, "/api/run", "analyst"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task Chat_allowed_for_client_readonly_but_not_viewer() + { + using var factory = new BffFactory(secret: Secret); + var ok = await Send(factory, HttpMethod.Post, "/api/chat", "client-readonly"); + Assert.Equal(HttpStatusCode.OK, ok.StatusCode); + + var forbidden = await Send(factory, HttpMethod.Post, "/api/chat", "viewer"); + Assert.Equal(HttpStatusCode.Forbidden, forbidden.StatusCode); + } + + [Fact] + public async Task Session_endpoint_reflects_role() + { + using var factory = new BffFactory(secret: Secret); + var response = await Send(factory, HttpMethod.Get, "/api/auth/session", "analyst"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.True(doc.RootElement.GetProperty("authEnabled").GetBoolean()); + Assert.True(doc.RootElement.GetProperty("authenticated").GetBoolean()); + Assert.Equal("analyst", doc.RootElement.GetProperty("role").GetString()); + Assert.True(doc.RootElement.GetProperty("canMutate").GetBoolean()); + } + + [Fact] + public async Task Pdf_export_preserves_content_type_and_disposition() + { + using var factory = new BffFactory(); + var client = factory.CreateClient(); + var response = await client.GetAsync("/api/report/export?format=pdf&reportId=1"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("application/pdf", response.Content.Headers.ContentType?.MediaType); + Assert.NotNull(response.Content.Headers.ContentDisposition); + } + + [Fact] + public async Task Csv_export_routes_to_file_service() + { + using var factory = new BffFactory(); + var client = factory.CreateClient(); + var response = await client.GetAsync("/api/report/export?format=csv&reportId=1"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + // Body echoes the forwarded path → proves the BFF rewrote to the FileService csv route. + Assert.Contains("/v1/reports/1/csv", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task Json_export_routes_to_file_service() + { + using var factory = new BffFactory(); + var client = factory.CreateClient(); + var response = await client.GetAsync("/api/report/export?format=json&reportId=1"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("/v1/reports/1/json", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task Sitemap_export_routes_to_file_service() + { + using var factory = new BffFactory(); + var client = factory.CreateClient(); + var response = await client.GetAsync("/api/report/export-sitemap?reportId=1"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("/v1/reports/1/sitemap", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task Export_by_domain_routes_to_file_service() + { + using var factory = new BffFactory(); + var client = factory.CreateClient(); + var response = await client.GetAsync("/api/report/export?format=csv&domain=example.com"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("/v1/reports/by-domain/example.com/csv", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task Unsupported_export_format_returns_400() + { + using var factory = new BffFactory(); + var client = factory.CreateClient(); + var response = await client.GetAsync("/api/report/export?format=xml&reportId=1"); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task Redirect_location_header_is_passed_through() + { + using var factory = new BffFactory(); + var client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); + var response = await client.GetAsync("/api/integrations/google/auth?propertyId=1"); + Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); + Assert.Equal( + "https://accounts.google.com/o/oauth2/v2/auth?client_id=x", + response.Headers.Location?.ToString()); + } + + [Fact] + public async Task Cors_preflight_echoes_origin_with_credentials() + { + using var factory = new BffFactory(); + var client = factory.CreateClient(); + var request = new HttpRequestMessage(HttpMethod.Options, "/api/report/meta"); + request.Headers.Add("Origin", "http://localhost:3000"); + request.Headers.Add("Access-Control-Request-Method", "GET"); + var response = await client.SendAsync(request); + + Assert.Equal("http://localhost:3000", response.Headers.GetValues("Access-Control-Allow-Origin").Single()); + Assert.Equal("true", response.Headers.GetValues("Access-Control-Allow-Credentials").Single()); + } + + [Fact] + public async Task Get_routes_to_data_service_when_path_in_allowlist() + { + using var factory = new BffFactory(dataRoutes: "/api/report/meta"); + var client = factory.CreateClient(); + var response = await client.GetAsync("/api/report/meta"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("\"upstream\":\"data\"", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task Get_portfolio_routes_to_data_service_when_path_in_allowlist() + { + using var factory = new BffFactory(dataRoutes: "/api/report/portfolio"); + var client = factory.CreateClient(); + var response = await client.GetAsync("/api/report/portfolio?widget=groups"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("\"upstream\":\"data\"", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task Delete_portfolio_routes_to_data_service_when_path_in_allowlist() + { + using var factory = new BffFactory(secret: Secret, dataRoutes: "/api/portfolio"); + var client = factory.CreateClient(); + var request = new HttpRequestMessage(HttpMethod.Delete, "/api/portfolio/delete") + { + Content = JsonContent.Create(new { crawlRunId = 1L }), + }; + var token = WpSessionTokens.Create("analyst", Secret, DateTimeOffset.UtcNow.ToUnixTimeSeconds(), 604800); + request.Headers.Add("Cookie", $"{WpSessionTokens.CookieName}={token}"); + var response = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("\"upstream\":\"data\"", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task Get_issues_status_routes_to_data_service_when_path_in_allowlist() + { + using var factory = new BffFactory(dataRoutes: "/api/issues/status"); + var client = factory.CreateClient(); + var response = await client.GetAsync("/api/issues/status?propertyId=1"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("\"upstream\":\"data\"", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task Put_issues_status_routes_to_data_service_when_path_in_allowlist() + { + using var factory = new BffFactory(secret: Secret, dataRoutes: "/api/issues/status"); + var client = factory.CreateClient(); + var request = new HttpRequestMessage(HttpMethod.Put, "/api/issues/status") + { + Content = JsonContent.Create(new + { + propertyId = 1L, + message = "Missing meta description", + status = "open", + }), + }; + var token = WpSessionTokens.Create("analyst", Secret, DateTimeOffset.UtcNow.ToUnixTimeSeconds(), 604800); + request.Headers.Add("Cookie", $"{WpSessionTokens.CookieName}={token}"); + var response = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("\"upstream\":\"data\"", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task Put_issues_status_forbidden_for_readonly_role() + { + using var factory = new BffFactory(secret: Secret, dataRoutes: "/api/issues/status"); + var response = await Send(factory, HttpMethod.Put, "/api/issues/status", "client-readonly"); + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task Get_filters_routes_to_data_service_when_path_in_allowlist() + { + using var factory = new BffFactory(dataRoutes: "/api/filters"); + var client = factory.CreateClient(); + var response = await client.GetAsync("/api/filters?propertyId=1"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("\"upstream\":\"data\"", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task Post_filters_routes_to_data_service_when_path_in_allowlist() + { + using var factory = new BffFactory(secret: Secret, dataRoutes: "/api/filters"); + var client = factory.CreateClient(); + var request = new HttpRequestMessage(HttpMethod.Post, "/api/filters") + { + Content = JsonContent.Create(new + { + propertyId = 1L, + name = "my-filter", + filterJson = new { status = new[] { "200" } }, + }), + }; + var token = WpSessionTokens.Create("analyst", Secret, DateTimeOffset.UtcNow.ToUnixTimeSeconds(), 604800); + request.Headers.Add("Cookie", $"{WpSessionTokens.CookieName}={token}"); + var response = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("\"upstream\":\"data\"", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task Post_filters_forbidden_for_readonly_role() + { + using var factory = new BffFactory(secret: Secret, dataRoutes: "/api/filters"); + var response = await Send(factory, HttpMethod.Post, "/api/filters", "client-readonly"); + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task Delete_filters_routes_to_data_service_when_path_in_allowlist() + { + using var factory = new BffFactory(secret: Secret, dataRoutes: "/api/filters"); + var client = factory.CreateClient(); + var request = new HttpRequestMessage(HttpMethod.Delete, "/api/filters") + { + Content = JsonContent.Create(new { propertyId = 1L, name = "my-filter" }), + }; + var token = WpSessionTokens.Create("analyst", Secret, DateTimeOffset.UtcNow.ToUnixTimeSeconds(), 604800); + request.Headers.Add("Cookie", $"{WpSessionTokens.CookieName}={token}"); + var response = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("\"upstream\":\"data\"", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task Delete_filters_forbidden_for_readonly_role() + { + using var factory = new BffFactory(secret: Secret, dataRoutes: "/api/filters"); + var response = await Send(factory, HttpMethod.Delete, "/api/filters", "client-readonly"); + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task Post_issues_action_plan_stays_on_fastapi_when_issues_prefix_narrowed() + { + using var factory = new BffFactory(secret: Secret, dataRoutes: "/api/issues/status,/api/filters"); + var client = factory.CreateClient(); + var request = new HttpRequestMessage(HttpMethod.Post, "/api/issues/action-plan") + { + Content = JsonContent.Create(new { domain = "example.com", issues = new[] { new { message = "x" } } }), + }; + var token = WpSessionTokens.Create("analyst", Secret, DateTimeOffset.UtcNow.ToUnixTimeSeconds(), 604800); + request.Headers.Add("Cookie", $"{WpSessionTokens.CookieName}={token}"); + var response = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.DoesNotContain("\"upstream\":\"data\"", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task Get_stays_on_fastapi_when_path_not_in_allowlist() + { + using var factory = new BffFactory(dataRoutes: "/api/report/portfolio"); + var client = factory.CreateClient(); + var response = await client.GetAsync("/api/report/meta"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.DoesNotContain("\"upstream\":\"data\"", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task Empty_allowlist_keeps_all_reads_on_fastapi() + { + using var factory = new BffFactory(); + var client = factory.CreateClient(); + var response = await client.GetAsync("/api/report/meta"); + Assert.DoesNotContain("\"upstream\":\"data\"", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task Data_routed_get_still_requires_authentication() + { + using var factory = new BffFactory(secret: Secret, dataRoutes: "/api/report/meta"); + var client = factory.CreateClient(); + var response = await client.GetAsync("/api/report/meta"); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact] + public async Task Data_routed_get_allowed_for_readonly_role_and_hits_data() + { + using var factory = new BffFactory(secret: Secret, dataRoutes: "/api/report/meta"); + var response = await Send(factory, HttpMethod.Get, "/api/report/meta", "viewer"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("\"upstream\":\"data\"", await response.Content.ReadAsStringAsync()); + } + + private static async Task Send(BffFactory factory, HttpMethod method, string path, string role) + { + var client = factory.CreateClient(); + var request = new HttpRequestMessage(method, path); + var token = WpSessionTokens.Create(role, Secret, DateTimeOffset.UtcNow.ToUnixTimeSeconds(), 604800); + request.Headers.Add("Cookie", $"{WpSessionTokens.CookieName}={token}"); + return await client.SendAsync(request); + } +} + +internal sealed class BffFactory(string? secret = null, string? dataRoutes = null) : WebApplicationFactory +{ + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.UseEnvironment("Production"); + builder.ConfigureAppConfiguration((_, cfg) => + { + var settings = new Dictionary { ["Auth:Secret"] = secret ?? string.Empty }; + if (dataRoutes is not null) + { + settings["Upstream:DataRoutes:0"] = dataRoutes; + } + cfg.AddInMemoryCollection(settings); + }); + builder.ConfigureServices(services => + { + foreach (var name in new[] + { + DependencyInjection.FastApiClient, + DependencyInjection.FastApiStreamClient, + DependencyInjection.FileServiceClient, + }) + { + services.AddHttpClient(name) + .ConfigurePrimaryHttpMessageHandler(() => new TestHttpHandler(Respond)); + } + // Stub the Data service with a distinguishable body so cutover routing can be asserted. + services.AddHttpClient(DependencyInjection.DataClient) + .ConfigurePrimaryHttpMessageHandler(() => new TestHttpHandler(RespondData)); + }); + } + + private static HttpResponseMessage RespondData(HttpRequestMessage request) => + TestHttpHandler.Json($"{{\"path\":\"{request.RequestUri!.AbsolutePath}\",\"upstream\":\"data\"}}"); + + private static HttpResponseMessage Respond(HttpRequestMessage request) + { + var path = request.RequestUri!.AbsolutePath; + if (path.Contains("/google/auth")) + { + // Simulate FastAPI's OAuth consent 302 — the forwarder must pass Location through. + var redirect = new HttpResponseMessage(HttpStatusCode.Redirect); + redirect.Headers.Location = new Uri("https://accounts.google.com/o/oauth2/v2/auth?client_id=x"); + return redirect; + } + if (path.Contains("/pdf") || path.Contains("/workbook")) + { + var resp = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayContent("%PDF-1.4 fake"u8.ToArray()), + }; + resp.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf"); + resp.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") + { + FileName = "audit.pdf", + }; + return resp; + } + return TestHttpHandler.Json($"{{\"path\":\"{path}\",\"ok\":true}}"); + } +} diff --git a/services/Bff/tests/Bff.Tests/IdempotentRetryHandlerTests.cs b/services/Bff/tests/Bff.Tests/IdempotentRetryHandlerTests.cs new file mode 100644 index 00000000..82a30c25 --- /dev/null +++ b/services/Bff/tests/Bff.Tests/IdempotentRetryHandlerTests.cs @@ -0,0 +1,59 @@ +using System.Net; +using Bff.Application.Http; + +namespace Bff.Tests; + +public class IdempotentRetryHandlerTests +{ + [Fact] + public async Task Retries_transient_failures_for_GET() + { + var inner = new CountingHandler(HttpStatusCode.ServiceUnavailable); + using var invoker = new HttpMessageInvoker(new IdempotentRetryHandler { InnerHandler = inner }); + + using var response = await invoker.SendAsync( + new HttpRequestMessage(HttpMethod.Get, "http://upstream/x"), CancellationToken.None); + + Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); + Assert.Equal(3, inner.Count); // initial + 2 retries + } + + [Fact] + public async Task Does_not_retry_POST() + { + var inner = new CountingHandler(HttpStatusCode.ServiceUnavailable); + using var invoker = new HttpMessageInvoker(new IdempotentRetryHandler { InnerHandler = inner }); + + using var response = await invoker.SendAsync( + new HttpRequestMessage(HttpMethod.Post, "http://upstream/x"), CancellationToken.None); + + Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); + Assert.Equal(1, inner.Count); // never retried — mutations must not double-submit + } + + [Fact] + public async Task Does_not_retry_successful_GET() + { + var inner = new CountingHandler(HttpStatusCode.OK); + using var invoker = new HttpMessageInvoker(new IdempotentRetryHandler { InnerHandler = inner }); + + using var response = await invoker.SendAsync( + new HttpRequestMessage(HttpMethod.Get, "http://upstream/x"), CancellationToken.None); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(1, inner.Count); + } + + private sealed class CountingHandler(HttpStatusCode status) : HttpMessageHandler + { + public int Count { get; private set; } + + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + Count++; + return Task.FromResult(new HttpResponseMessage(status)); + } + } +} diff --git a/services/Bff/tests/Bff.Tests/TestHttpHandler.cs b/services/Bff/tests/Bff.Tests/TestHttpHandler.cs new file mode 100644 index 00000000..07371cf5 --- /dev/null +++ b/services/Bff/tests/Bff.Tests/TestHttpHandler.cs @@ -0,0 +1,24 @@ +using System.Net; +using System.Text; + +namespace Bff.Tests; + +/// Stub upstream handler for swapping into the BFF's named HttpClients during tests. +public sealed class TestHttpHandler(Func responder) : HttpMessageHandler +{ + public List Requests { get; } = []; + + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + Requests.Add(request); + return Task.FromResult(responder(request)); + } + + public static HttpResponseMessage Json(string body, HttpStatusCode status = HttpStatusCode.OK) => + new(status) + { + Content = new StringContent(body, Encoding.UTF8, "application/json"), + }; +} diff --git a/services/Bff/tests/Bff.Tests/WpSessionTokensTests.cs b/services/Bff/tests/Bff.Tests/WpSessionTokensTests.cs new file mode 100644 index 00000000..4316df4a --- /dev/null +++ b/services/Bff/tests/Bff.Tests/WpSessionTokensTests.cs @@ -0,0 +1,104 @@ +using Bff.Application.Auth; + +namespace Bff.Tests; + +/// +/// Byte-compatibility tests for wp_session. These are the trust anchor for "sessions survive +/// the cutover" — the golden vectors are HMACs produced by the same algorithm as auth.ts. +/// +public class WpSessionTokensTests +{ + private const string Secret = "test-secret-123"; + + // Vectors produced exactly as web/src/server/auth.ts would: "{role}:{exp}.{hmacSha256Hex(secret, payload)}". + private const string AnalystToken = "analyst:9999999999.7b9413bfc6f167f189d749e6105bdee01e202e445e86cce18acc49aeb2b2c338"; + private const string AdminToken = "admin:9999999999.f4d51fd58e594e247afc3472d5a73972a38e30f9013f0a1686f8719d24f705e6"; + private const string TrailingGarbageExpToken = "analyst:9999999999abc.57f60696d1f73f95b888c6b22865f562d473a2e3d3fcf5c8b3c3a953583487ef"; + + [Fact] + public void Verify_accepts_node_produced_golden_vectors() + { + Assert.Equal("analyst", WpSessionTokens.VerifyRole(AnalystToken, Secret, nowUnixSeconds: 1000)); + Assert.Equal("admin", WpSessionTokens.VerifyRole(AdminToken, Secret, nowUnixSeconds: 1000)); + } + + [Fact] + public void Verify_accepts_trailing_garbage_exp_like_js_parseInt() + { + // Node parseInt("9999999999abc", 10) === 9999999999 (valid, future). C# must agree. + Assert.Equal("analyst", WpSessionTokens.VerifyRole(TrailingGarbageExpToken, Secret, nowUnixSeconds: 1000)); + } + + [Fact] + public void Verify_is_case_insensitive_on_hex_signature() + { + var upper = AnalystToken.ToUpperInvariant(); // payload also uppercased, but role compare uses payload role 'ANALYST' + // Only the signature hex is case-insensitive; the payload (role) is part of the signed message, + // so uppercasing the whole token changes the payload and must fail. + Assert.Null(WpSessionTokens.VerifyRole(upper, Secret, 1000)); + + // Uppercasing ONLY the signature half must still validate (Node hex decode is case-insensitive). + var dot = AnalystToken.IndexOf('.'); + var sigUpper = AnalystToken[..(dot + 1)] + AnalystToken[(dot + 1)..].ToUpperInvariant(); + Assert.Equal("analyst", WpSessionTokens.VerifyRole(sigUpper, Secret, 1000)); + } + + [Fact] + public void Verify_rejects_tampered_signature() + { + var tampered = AnalystToken[..^1] + (AnalystToken[^1] == 'a' ? 'b' : 'a'); + Assert.Null(WpSessionTokens.VerifyRole(tampered, Secret, 1000)); + } + + [Fact] + public void Verify_rejects_expired_token() + { + var token = WpSessionTokens.Create("analyst", Secret, nowUnixSeconds: 1000, maxAgeSeconds: 100); + Assert.Equal("analyst", WpSessionTokens.VerifyRole(token, Secret, nowUnixSeconds: 1050)); + Assert.Null(WpSessionTokens.VerifyRole(token, Secret, nowUnixSeconds: 2000)); + } + + [Theory] + [InlineData("")] + [InlineData("no-dot")] + [InlineData("too.many.dots")] + [InlineData("analyst:9999999999.")] + public void Verify_rejects_malformed_tokens(string token) + { + Assert.Null(WpSessionTokens.VerifyRole(token, Secret, 1000)); + } + + [Fact] + public void Verify_returns_null_when_secret_empty() + { + Assert.Null(WpSessionTokens.VerifyRole(AnalystToken, secret: "", nowUnixSeconds: 1000)); + } + + [Fact] + public void Create_then_verify_roundtrips() + { + var token = WpSessionTokens.Create("editor", Secret, nowUnixSeconds: 1000, maxAgeSeconds: 604800); + Assert.Equal("editor", WpSessionTokens.VerifyRole(token, Secret, nowUnixSeconds: 1000)); + } + + [Theory] + [InlineData("123abc", 123)] + [InlineData(" 42 ", 42)] + [InlineData("-5", -5)] + [InlineData("+7", 7)] + [InlineData("0", 0)] + public void JsParseInt_matches_javascript(string input, long expected) + { + Assert.Equal(expected, WpSessionTokens.JsParseInt(input)); + } + + [Theory] + [InlineData("abc")] + [InlineData("")] + [InlineData(" ")] + [InlineData("+")] + public void JsParseInt_returns_null_for_nan(string input) + { + Assert.Null(WpSessionTokens.JsParseInt(input)); + } +} diff --git a/services/Data/.dockerignore b/services/Data/.dockerignore new file mode 100644 index 00000000..ab41d99e --- /dev/null +++ b/services/Data/.dockerignore @@ -0,0 +1,4 @@ +**/bin/ +**/obj/ +**/.vs/ +**/TestResults/ diff --git a/services/Data/Data.slnx b/services/Data/Data.slnx new file mode 100644 index 00000000..c965319b --- /dev/null +++ b/services/Data/Data.slnx @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/services/Data/Dockerfile b/services/Data/Dockerfile new file mode 100644 index 00000000..f6640731 --- /dev/null +++ b/services/Data/Dockerfile @@ -0,0 +1,20 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY Data.slnx ./ +COPY src/Data.Domain/Data.Domain.csproj src/Data.Domain/ +COPY src/Data.Application/Data.Application.csproj src/Data.Application/ +COPY src/Data.Api/Data.Api.csproj src/Data.Api/ +RUN dotnet restore src/Data.Api/Data.Api.csproj +COPY src/ src/ +RUN dotnet publish src/Data.Api/Data.Api.csproj -c Release -o /app/publish --no-restore + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime +WORKDIR /app +# curl is used by the compose healthcheck; the aspnet image does not ship it. +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl \ + && rm -rf /var/lib/apt/lists/* +ENV ASPNETCORE_URLS=http://+:8091 +EXPOSE 8091 +COPY --from=build /app/publish . +ENTRYPOINT ["dotnet", "Data.Api.dll"] diff --git a/services/Data/src/Data.Api/Controllers/FiltersController.cs b/services/Data/src/Data.Api/Controllers/FiltersController.cs new file mode 100644 index 00000000..3cea7053 --- /dev/null +++ b/services/Data/src/Data.Api/Controllers/FiltersController.cs @@ -0,0 +1,70 @@ +using System.Text.Json; +using Data.Application.Dto.Filters; +using Data.Application.Repositories; +using Microsoft.AspNetCore.Mvc; + +namespace Data.Api.Controllers; + +/// +/// Saved crawl filter endpoints ported from FastAPI's /api/filters. +/// +[ApiController] +[Route("api/filters")] +[Tags("Filters")] +public sealed class FiltersController : ControllerBase +{ + private readonly ISavedFilterRepository _filters; + + public FiltersController(ISavedFilterRepository filters) => _filters = filters; + + /// List saved filter presets for a property. + [HttpGet] + [ProducesResponseType(typeof(SavedFilterListResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task List([FromQuery] int? propertyId, CancellationToken cancellationToken) + { + if (propertyId is null or 0) + return BadRequest(new { detail = "propertyId required" }); + + var filters = await _filters.ListAsync(propertyId.Value, cancellationToken); + return Ok(new SavedFilterListResponse { Filters = filters }); + } + + /// Create or update a saved filter preset. + [HttpPost] + [ProducesResponseType(typeof(SavedFilterOkResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task Upsert( + [FromBody] UpsertSavedFilterRequest body, CancellationToken cancellationToken) + { + var name = (body.Name ?? string.Empty).Trim(); + if (body.PropertyId == 0 || string.IsNullOrEmpty(name)) + return BadRequest(new { detail = "propertyId and name required" }); + + var filterJson = body.FilterJson is { ValueKind: JsonValueKind.Object } element + ? element + : JsonSerializer.SerializeToElement(new { }); + + await _filters.UpsertAsync(body.PropertyId, name, filterJson, cancellationToken); + return Ok(new SavedFilterOkResponse()); + } + + /// Delete a saved filter preset by name. + [HttpDelete] + [ProducesResponseType(typeof(SavedFilterOkResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Delete( + [FromBody] DeleteSavedFilterRequest body, CancellationToken cancellationToken) + { + var name = (body.Name ?? string.Empty).Trim(); + if (body.PropertyId == 0 || string.IsNullOrEmpty(name)) + return BadRequest(new { detail = "propertyId and name required" }); + + var deleted = await _filters.DeleteAsync(body.PropertyId, name, cancellationToken); + if (!deleted) + return NotFound(new { detail = "filter not found" }); + + return Ok(new SavedFilterOkResponse()); + } +} diff --git a/services/Data/src/Data.Api/Controllers/HealthController.cs b/services/Data/src/Data.Api/Controllers/HealthController.cs new file mode 100644 index 00000000..9e3e1dc0 --- /dev/null +++ b/services/Data/src/Data.Api/Controllers/HealthController.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Data.Api.Controllers; + +[ApiController] +[Route("")] +[Tags("Health")] +public sealed class HealthController : ControllerBase +{ + [HttpGet("health")] + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult Get() => Ok(new { status = "ok" }); +} diff --git a/services/Data/src/Data.Api/Controllers/IssuesController.cs b/services/Data/src/Data.Api/Controllers/IssuesController.cs new file mode 100644 index 00000000..b46c9a08 --- /dev/null +++ b/services/Data/src/Data.Api/Controllers/IssuesController.cs @@ -0,0 +1,61 @@ +using Data.Application.Dto.Issues; +using Data.Application.Repositories; +using Microsoft.AspNetCore.Mvc; + +namespace Data.Api.Controllers; + +/// +/// Issue workflow endpoints ported from FastAPI's /api/issues/* (status only; LLM routes stay on FastAPI). +/// +[ApiController] +[Route("api/issues")] +[Tags("Issues")] +public sealed class IssuesController : ControllerBase +{ + private readonly IIssueStatusRepository _issues; + + public IssuesController(IIssueStatusRepository issues) => _issues = issues; + + /// List workflow status rows for a property. + [HttpGet("status")] + [ProducesResponseType(typeof(IssueStatusListResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task List([FromQuery] int? propertyId, CancellationToken cancellationToken) + { + if (propertyId is null or 0) + return BadRequest(new { detail = "propertyId required" }); + + var issues = await _issues.ListAsync(propertyId.Value, cancellationToken); + return Ok(new IssueStatusListResponse { Issues = issues }); + } + + /// Create or update workflow status for an issue fingerprint. + [HttpPut("status")] + [ProducesResponseType(typeof(IssueStatusUpsertResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task Upsert( + [FromBody] UpsertIssueStatusRequest body, CancellationToken cancellationToken) + { + var message = (body.Message ?? string.Empty).Trim(); + var status = body.Status ?? string.Empty; + + if (body.PropertyId == 0 || string.IsNullOrEmpty(message) || string.IsNullOrEmpty(status)) + return BadRequest(new { detail = "propertyId, message, and valid status required" }); + + try + { + body.Message = message; + var issue = await _issues.UpsertAsync(body, cancellationToken); + return Ok(new IssueStatusUpsertResponse { Issue = issue }); + } + catch (ArgumentException ex) + { + return BadRequest(new { detail = ex.Message }); + } + catch (InvalidOperationException) + { + return StatusCode(StatusCodes.Status500InternalServerError, new { detail = "issue status upsert failed" }); + } + } +} diff --git a/services/Data/src/Data.Api/Controllers/PortfolioController.cs b/services/Data/src/Data.Api/Controllers/PortfolioController.cs new file mode 100644 index 00000000..2f92c72d --- /dev/null +++ b/services/Data/src/Data.Api/Controllers/PortfolioController.cs @@ -0,0 +1,37 @@ +using Data.Application.Dto.Portfolio; +using Data.Application.Repositories; +using Microsoft.AspNetCore.Mvc; + +namespace Data.Api.Controllers; + +/// +/// Portfolio mutations ported from FastAPI's /api/portfolio/*. +/// +[ApiController] +[Route("api/portfolio")] +[Tags("Portfolio")] +public sealed class PortfolioController : ControllerBase +{ + private readonly IPortfolioRepository _portfolio; + + public PortfolioController(IPortfolioRepository portfolio) => _portfolio = portfolio; + + /// Delete a report and/or crawl run from the portfolio home list. + [HttpDelete("delete")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Delete([FromBody] DeletePortfolioRequest body, CancellationToken cancellationToken) + { + if (body.ReportId is null && body.CrawlRunId is null) + return BadRequest(new { detail = "reportId or crawlRunId required" }); + + var deleted = await _portfolio.DeletePortfolioItemAsync( + body.ReportId, body.CrawlRunId, cancellationToken); + + if (!deleted) + return NotFound(new { detail = "portfolio item not found" }); + + return Ok(new { ok = true }); + } +} diff --git a/services/Data/src/Data.Api/Controllers/ReportController.cs b/services/Data/src/Data.Api/Controllers/ReportController.cs new file mode 100644 index 00000000..8320a715 --- /dev/null +++ b/services/Data/src/Data.Api/Controllers/ReportController.cs @@ -0,0 +1,157 @@ +using System.Text.Json; +using Data.Application.Dto.Meta; +using Data.Application.Dto.Portfolio; +using Data.Application.Dto.Report; +using Data.Application.Portfolio; +using Data.Application.Report; +using Data.Application.Repositories; +using Microsoft.AspNetCore.Mvc; + +namespace Data.Api.Controllers; + +/// +/// Read endpoints ported from FastAPI's /api/report/*. The BFF forwards the full path +/// here when it is listed in DATA_ROUTES, so the routes must match the FastAPI paths exactly. +/// +[ApiController] +[Route("api/report")] +[Tags("Report")] +public sealed class ReportController : ControllerBase +{ + private readonly IReportRepository _reports; + private readonly IPortfolioService _portfolio; + + public ReportController(IReportRepository reports, IPortfolioService portfolio) + { + _reports = reports; + _portfolio = portfolio; + } + + /// Report list + crawl-run list (snake_case keys). + [HttpGet("meta")] + [ProducesResponseType(typeof(ReportMetaResponse), StatusCodes.Status200OK)] + public async Task> GetMeta(CancellationToken cancellationToken) => + Ok(await _reports.GetMetaAsync(cancellationToken)); + + /// + /// Returns the raw JSONB payload, optionally sliced to a named section. + /// + [HttpGet("payload")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetPayload( + [FromQuery] long? reportId, + [FromQuery] string? domain, + [FromQuery] string? section, + CancellationToken cancellationToken) + { + if (section is not null && !SectionFields.ValidKeys.Contains(section)) + return StatusCode(StatusCodes.Status400BadRequest, new { detail = "Invalid section" }); + + var rawJson = await _reports.GetPayloadDataAsync(reportId, domain, cancellationToken); + if (rawJson is null) + return NotFound(new { detail = "Report not found" }); + + if (section is not null) + { + using var doc = JsonDocument.Parse(rawJson); + var filtered = new Dictionary(); + foreach (var field in SectionFields.ByKey[section]) + if (doc.RootElement.TryGetProperty(field, out var val)) + filtered[field] = val.Clone(); + return Ok(new { payload = filtered, section }); + } + + // Full payload: stream raw JSON without double-parsing (avoids re-serialising multi-MB blobs). + return Content($"{{\"payload\":{rawJson}}}", "application/json"); + } + + /// + /// Ordered audit history, optional domain filter. + /// is accepted but ignored — report_payload has no property_id column. + /// + [HttpGet("history")] + [ProducesResponseType(typeof(AuditHistoryResponse), StatusCodes.Status200OK)] + public async Task> GetHistory( + [FromQuery] int? propertyId, + [FromQuery] string? domain, + [FromQuery] int limit, + CancellationToken cancellationToken) + { + if (limit <= 0) limit = 20; + return Ok(await _reports.ListAuditHistoryAsync(domain, limit, cancellationToken)); + } + + [HttpGet("crawl-payload")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetCrawlPayload( + [FromQuery] long? crawlRunId, + CancellationToken cancellationToken) + { + if (crawlRunId is null or <= 0) + return StatusCode(StatusCodes.Status400BadRequest, new { detail = "Invalid crawlRunId" }); + + var payload = await _reports.GetCrawlPreviewPayloadAsync(crawlRunId.Value, cancellationToken); + if (payload is null) + return NotFound(new { detail = "Crawl run not found" }); + + return Ok(new { payload }); + } + + [HttpGet("mobile-delta")] + [ProducesResponseType(typeof(MobileDeltaResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task> GetMobileDelta( + [FromQuery] long? id, + CancellationToken cancellationToken) + { + if (id is null or <= 0) + return BadRequest(new { detail = "id required" }); + + return Ok(await _reports.GetMobileDeltaAsync(id.Value, cancellationToken)); + } + + /// Portfolio groups, crawl history, summary, or single card widget. + [HttpGet("portfolio")] + [ProducesResponseType(typeof(PortfolioGroupsResponseDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(PortfolioCardResponseDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(PortfolioSummaryResponseDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task GetPortfolio( + [FromQuery] string widget = "full", + [FromQuery] string? ids = null, + [FromQuery] long? reportId = null, + [FromQuery] long? crawlRunId = null, + CancellationToken cancellationToken = default) + { + if (!PortfolioConstants.ValidWidgets.Contains(widget)) + return StatusCode(StatusCodes.Status400BadRequest, new { detail = "Invalid widget" }); + + if (widget.Equals("card", StringComparison.OrdinalIgnoreCase) && + reportId is null && crawlRunId is null) + { + return StatusCode(StatusCodes.Status400BadRequest, + new { detail = "reportId or crawlRunId required for card widget" }); + } + + var idList = ParseIds(ids); + var result = await _portfolio.GetPortfolioResponseAsync( + widget, idList, reportId, crawlRunId, cancellationToken); + return Ok(result); + } + + private static List ParseIds(string? ids) + { + if (string.IsNullOrWhiteSpace(ids)) return []; + var list = new List(); + foreach (var part in ids.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + if (long.TryParse(part, out var n) && n > 0) + list.Add(n); + } + return list; + } +} diff --git a/services/Data/src/Data.Api/Data.Api.csproj b/services/Data/src/Data.Api/Data.Api.csproj new file mode 100644 index 00000000..81e09679 --- /dev/null +++ b/services/Data/src/Data.Api/Data.Api.csproj @@ -0,0 +1,18 @@ + + + + + + + + + + + + + net10.0 + enable + enable + + + diff --git a/services/Data/src/Data.Api/Program.cs b/services/Data/src/Data.Api/Program.cs new file mode 100644 index 00000000..c039e823 --- /dev/null +++ b/services/Data/src/Data.Api/Program.cs @@ -0,0 +1,38 @@ +using Data.Application; +using Microsoft.OpenApi; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDataApplication(); +builder.Services.AddControllers(); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(options => +{ + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "Website Profiling Data API", + Version = "v1", + Description = + "Internal read-only data service. Reads Postgres directly and incrementally replaces " + + "FastAPI read endpoints. Reached only by the BFF (not browser-facing).", + }); +}); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/swagger/v1/swagger.json", "Website Profiling Data API v1"); + options.RoutePrefix = "docs"; + }); +} + +app.MapControllers(); + +app.Run(); + +public partial class Program; diff --git a/services/Data/src/Data.Api/appsettings.Development.json b/services/Data/src/Data.Api/appsettings.Development.json new file mode 100644 index 00000000..39d4c23b --- /dev/null +++ b/services/Data/src/Data.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.EntityFrameworkCore": "Information" + } + } +} diff --git a/services/Data/src/Data.Api/appsettings.json b/services/Data/src/Data.Api/appsettings.json new file mode 100644 index 00000000..943c46fc --- /dev/null +++ b/services/Data/src/Data.Api/appsettings.json @@ -0,0 +1,17 @@ +{ + "Urls": "http://127.0.0.1:8091", + "Database": { + "ConnectionString": "", + "MinPoolSize": 2, + "MaxPoolSize": 20, + "CommandTimeoutSeconds": 30 + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/services/Data/src/Data.Application/Data.Application.csproj b/services/Data/src/Data.Application/Data.Application.csproj new file mode 100644 index 00000000..29701a6a --- /dev/null +++ b/services/Data/src/Data.Application/Data.Application.csproj @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + net10.0 + enable + enable + + + + + + + diff --git a/services/Data/src/Data.Application/DependencyInjection.cs b/services/Data/src/Data.Application/DependencyInjection.cs new file mode 100644 index 00000000..18bbad1a --- /dev/null +++ b/services/Data/src/Data.Application/DependencyInjection.cs @@ -0,0 +1,59 @@ +using Data.Application.Options; +using Data.Application.Persistence; +using Data.Application.Portfolio; +using Data.Application.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Npgsql; + +namespace Data.Application; + +public static class DependencyInjection +{ + /// + /// Registers the read-only data layer: a shared (pooled to mirror + /// the Python psycopg pool), a pooled DataDbContext with no-tracking queries, and the + /// repositories. The connection string comes from DATABASE_URL (libpq URI form). + /// + public static IServiceCollection AddDataApplication(this IServiceCollection services) + { + services.AddMemoryCache(); + services.AddOptions() + .BindConfiguration(DatabaseOptions.SectionName) + .PostConfigure(o => + { + var url = Environment.GetEnvironmentVariable("DATABASE_URL"); + if (!string.IsNullOrWhiteSpace(url)) + { + o.ConnectionString = url.Trim(); + } + }); + + services.AddSingleton(sp => + { + var o = sp.GetRequiredService>().Value; + var builder = new NpgsqlDataSourceBuilder(NpgsqlDsn.ToNpgsql(o.ConnectionString)); + builder.ConnectionStringBuilder.MinPoolSize = o.MinPoolSize; + builder.ConnectionStringBuilder.MaxPoolSize = o.MaxPoolSize; + return builder.Build(); + }); + + services.AddDbContextPool((sp, options) => + { + var o = sp.GetRequiredService>().Value; + var dataSource = sp.GetRequiredService(); + options + .UseNpgsql(dataSource, npg => npg.CommandTimeout(o.CommandTimeoutSeconds)) + .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); + }); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + return services; + } +} diff --git a/services/Data/src/Data.Application/Dto/Filters/SavedFilterDtos.cs b/services/Data/src/Data.Application/Dto/Filters/SavedFilterDtos.cs new file mode 100644 index 00000000..468741fe --- /dev/null +++ b/services/Data/src/Data.Application/Dto/Filters/SavedFilterDtos.cs @@ -0,0 +1,55 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Data.Application.Dto.Filters; + +public sealed class SavedFilterRowDto +{ + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("propertyId")] + public long PropertyId { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("filterJson")] + public JsonElement FilterJson { get; set; } + + [JsonPropertyName("createdAt")] + public string CreatedAt { get; set; } = string.Empty; +} + +public sealed class SavedFilterListResponse +{ + [JsonPropertyName("filters")] + public IReadOnlyList Filters { get; set; } = []; +} + +public sealed class SavedFilterOkResponse +{ + [JsonPropertyName("ok")] + public bool Ok { get; set; } = true; +} + +public sealed class UpsertSavedFilterRequest +{ + [JsonPropertyName("propertyId")] + public long PropertyId { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("filterJson")] + public JsonElement? FilterJson { get; set; } +} + +public sealed class DeleteSavedFilterRequest +{ + [JsonPropertyName("propertyId")] + public long PropertyId { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } +} diff --git a/services/Data/src/Data.Application/Dto/Issues/IssueStatusDtos.cs b/services/Data/src/Data.Application/Dto/Issues/IssueStatusDtos.cs new file mode 100644 index 00000000..f675b01a --- /dev/null +++ b/services/Data/src/Data.Application/Dto/Issues/IssueStatusDtos.cs @@ -0,0 +1,84 @@ +using System.Text.Json.Serialization; + +namespace Data.Application.Dto.Issues; + +public sealed class IssueStatusRowDto +{ + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("propertyId")] + public long PropertyId { get; set; } + + [JsonPropertyName("reportId")] + public long? ReportId { get; set; } + + [JsonPropertyName("issueFingerprint")] + public string IssueFingerprint { get; set; } = string.Empty; + + [JsonPropertyName("categoryId")] + public string? CategoryId { get; set; } + + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; + + [JsonPropertyName("url")] + public string Url { get; set; } = string.Empty; + + [JsonPropertyName("priority")] + public string Priority { get; set; } = string.Empty; + + [JsonPropertyName("status")] + public string Status { get; set; } = string.Empty; + + [JsonPropertyName("assignee")] + public string? Assignee { get; set; } + + [JsonPropertyName("note")] + public string? Note { get; set; } + + [JsonPropertyName("updatedAt")] + public string UpdatedAt { get; set; } = string.Empty; +} + +public sealed class IssueStatusListResponse +{ + [JsonPropertyName("issues")] + public IReadOnlyList Issues { get; set; } = []; +} + +public sealed class IssueStatusUpsertResponse +{ + [JsonPropertyName("issue")] + public IssueStatusRowDto Issue { get; set; } = new(); +} + +public sealed class UpsertIssueStatusRequest +{ + [JsonPropertyName("propertyId")] + public long PropertyId { get; set; } + + [JsonPropertyName("reportId")] + public long? ReportId { get; set; } + + [JsonPropertyName("message")] + public string? Message { get; set; } + + [JsonPropertyName("url")] + public string? Url { get; set; } + + [JsonPropertyName("priority")] + public string? Priority { get; set; } + + [JsonPropertyName("categoryId")] + public string? CategoryId { get; set; } + + [JsonPropertyName("status")] + public string? Status { get; set; } + + [JsonPropertyName("assignee")] + public string? Assignee { get; set; } + + [JsonPropertyName("note")] + public string? Note { get; set; } +} diff --git a/services/Data/src/Data.Application/Dto/Meta/ReportMetaResponse.cs b/services/Data/src/Data.Application/Dto/Meta/ReportMetaResponse.cs new file mode 100644 index 00000000..1336a3a1 --- /dev/null +++ b/services/Data/src/Data.Application/Dto/Meta/ReportMetaResponse.cs @@ -0,0 +1,54 @@ +using System.Text.Json.Serialization; + +namespace Data.Application.Dto.Meta; + +/// +/// Response shape for GET /api/report/meta. Mirrors the FastAPI router which returns +/// {"reports": list_reports(...), "crawlRuns": list_crawl_runs(...)}. Item keys are +/// snake_case (straight from SQL columns), so each property carries an explicit +/// — the service must NOT apply a global camelCase policy. +/// +public sealed class ReportMetaResponse +{ + [JsonPropertyName("reports")] + public required IReadOnlyList Reports { get; init; } + + [JsonPropertyName("crawlRuns")] + public required IReadOnlyList CrawlRuns { get; init; } +} + +/// One row of list_reports — snake_case keys. +public sealed class ReportListItem +{ + [JsonPropertyName("id")] + public long Id { get; init; } + + [JsonPropertyName("canonical_domain")] + public string? CanonicalDomain { get; init; } + + [JsonPropertyName("site_name")] + public string? SiteName { get; init; } + + [JsonPropertyName("generated_at")] + public string? GeneratedAt { get; init; } +} + +/// One row of list_crawl_runs — snake_case keys. +public sealed class CrawlRunItem +{ + [JsonPropertyName("id")] + public long Id { get; init; } + + /// Python coerces a null start_url to the empty string (str(... or "")). + [JsonPropertyName("start_url")] + public string StartUrl { get; init; } = ""; + + [JsonPropertyName("created_at")] + public string? CreatedAt { get; init; } + + [JsonPropertyName("render_mode")] + public string? RenderMode { get; init; } + + [JsonPropertyName("discovery_mode")] + public string? DiscoveryMode { get; init; } +} diff --git a/services/Data/src/Data.Application/Dto/Portfolio/DeletePortfolioRequest.cs b/services/Data/src/Data.Application/Dto/Portfolio/DeletePortfolioRequest.cs new file mode 100644 index 00000000..f74812be --- /dev/null +++ b/services/Data/src/Data.Application/Dto/Portfolio/DeletePortfolioRequest.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Data.Application.Dto.Portfolio; + +public sealed class DeletePortfolioRequest +{ + [JsonPropertyName("reportId")] + public long? ReportId { get; set; } + + [JsonPropertyName("crawlRunId")] + public long? CrawlRunId { get; set; } +} diff --git a/services/Data/src/Data.Application/Dto/Portfolio/PortfolioDtos.cs b/services/Data/src/Data.Application/Dto/Portfolio/PortfolioDtos.cs new file mode 100644 index 00000000..760e0495 --- /dev/null +++ b/services/Data/src/Data.Application/Dto/Portfolio/PortfolioDtos.cs @@ -0,0 +1,132 @@ +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace Data.Application.Dto.Portfolio; + +public sealed class PortfolioIssueCountsDto +{ + [JsonPropertyName("critical")] public int Critical { get; set; } + [JsonPropertyName("high")] public int High { get; set; } + [JsonPropertyName("medium")] public int Medium { get; set; } + [JsonPropertyName("low")] public int Low { get; set; } +} + +public sealed class PortfolioStatusCountsDto +{ + [JsonPropertyName("s2xx")] public int S2xx { get; set; } + [JsonPropertyName("s3xx")] public int S3xx { get; set; } + [JsonPropertyName("s4xx")] public int S4xx { get; set; } + [JsonPropertyName("s5xx")] public int S5xx { get; set; } + [JsonPropertyName("other")] public int Other { get; set; } +} + +public sealed class PortfolioCategorySnapshotDto +{ + [JsonPropertyName("id")] public string Id { get; set; } = ""; + [JsonPropertyName("name")] public string Name { get; set; } = ""; + [JsonPropertyName("score")] public int Score { get; set; } + [JsonPropertyName("issueCount")] public int IssueCount { get; set; } +} + +public sealed class PortfolioSeoSignalsDto +{ + [JsonPropertyName("missingTitles")] public int MissingTitles { get; set; } + [JsonPropertyName("missingMetaDesc")] public int MissingMetaDesc { get; set; } + [JsonPropertyName("thinContent")] public int ThinContent { get; set; } + [JsonPropertyName("h1Issues")] public int H1Issues { get; set; } +} + +public sealed class PortfolioGroupDto +{ + [JsonPropertyName("domainName")] public string DomainName { get; set; } = ""; + [JsonPropertyName("crawlUrl")] public string CrawlUrl { get; set; } = ""; + [JsonPropertyName("urlCount")] public int UrlCount { get; set; } + [JsonPropertyName("healthScore")] public int HealthScore { get; set; } + [JsonPropertyName("statusCounts")] public PortfolioStatusCountsDto StatusCounts { get; set; } = new(); + [JsonPropertyName("lastCrawl")] public string LastCrawl { get; set; } = ""; + [JsonPropertyName("lastAudit")] public string LastAudit { get; set; } = ""; + [JsonPropertyName("totalIssues")] public int TotalIssues { get; set; } + [JsonPropertyName("issueCounts")] public PortfolioIssueCountsDto IssueCounts { get; set; } = new(); + [JsonPropertyName("successRate")] public int? SuccessRate { get; set; } + [JsonPropertyName("titleCoverage")] public int? TitleCoverage { get; set; } + [JsonPropertyName("avgWordCount")] public int? AvgWordCount { get; set; } + [JsonPropertyName("thinPages")] public int? ThinPages { get; set; } + [JsonPropertyName("technicalSeoScore")] public int? TechnicalSeoScore { get; set; } + [JsonPropertyName("perfScore")] public int? PerfScore { get; set; } + [JsonPropertyName("seoScore")] public int? SeoScore { get; set; } + [JsonPropertyName("crawlDurationS")] public int? CrawlDurationS { get; set; } + [JsonPropertyName("categorySnapshots")] public IReadOnlyList CategorySnapshots { get; set; } = []; + [JsonPropertyName("seoSignals")] public PortfolioSeoSignalsDto? SeoSignals { get; set; } + [JsonPropertyName("securityFindings")] public int SecurityFindings { get; set; } + [JsonPropertyName("duplicateClusters")] public int DuplicateClusters { get; set; } + [JsonPropertyName("medianWordCount")] public int? MedianWordCount { get; set; } + [JsonPropertyName("medianResponseMs")] public int? MedianResponseMs { get; set; } + [JsonPropertyName("reportId")] public long? ReportId { get; set; } + [JsonPropertyName("crawlRunId")] public long? CrawlRunId { get; set; } + [JsonPropertyName("crawlOnly")] public bool? CrawlOnly { get; set; } + [JsonPropertyName("generatedAtMs")] public double GeneratedAtMs { get; set; } + [JsonPropertyName("domainParam")] public string DomainParam { get; set; } = ""; + [JsonPropertyName("crawlConfig")] public JsonNode? CrawlConfig { get; set; } + [JsonPropertyName("dataSources")] public IReadOnlyList? DataSources { get; set; } +} + +public sealed class PortfolioCrawlHistoryPointDto +{ + [JsonPropertyName("pagesDiscovered")] public int PagesDiscovered { get; set; } + [JsonPropertyName("titleCoverage")] public int TitleCoverage { get; set; } + [JsonPropertyName("avgWordCount")] public int AvgWordCount { get; set; } + [JsonPropertyName("createdAtMs")] public double CreatedAtMs { get; set; } +} + +public sealed class PortfolioGroupsResponseDto +{ + [JsonPropertyName("groups")] public IReadOnlyList Groups { get; set; } = []; + [JsonPropertyName("crawlHistoryByDomain")] public Dictionary> CrawlHistoryByDomain { get; set; } = []; +} + +public sealed class PortfolioCardResponseDto +{ + [JsonPropertyName("group")] public PortfolioGroupDto? Group { get; set; } +} + +public sealed class PortfolioSummaryResponseDto +{ + [JsonPropertyName("totalBrands")] public int TotalBrands { get; set; } + [JsonPropertyName("totalUrls")] public int TotalUrls { get; set; } + [JsonPropertyName("avgHealth")] public int? AvgHealth { get; set; } +} + +public sealed class PortfolioReportRow +{ + public long Id { get; init; } + public string? CanonicalDomain { get; init; } + public string? SiteName { get; init; } + public string? GeneratedAt { get; init; } +} + +public sealed class PortfolioCrawlRunRow +{ + public long Id { get; init; } + public string StartUrl { get; init; } = ""; + public string CreatedAt { get; init; } = ""; + public string? RenderMode { get; init; } + public string? DiscoveryMode { get; init; } +} + +public sealed class PortfolioCrawlSummaryRow +{ + public long CrawlRunId { get; init; } + public string StartUrl { get; init; } = ""; + public string CreatedAt { get; init; } = ""; + public int UrlCount { get; init; } + public int S2xx { get; init; } + public int S3xx { get; init; } + public int S4xx { get; init; } + public int S5xx { get; init; } + public int Other { get; init; } + public int WithTitle { get; init; } + public int AvgWordCount { get; init; } + public int ThinPages { get; init; } + public string? RenderMode { get; init; } + public string? DiscoveryMode { get; init; } +} diff --git a/services/Data/src/Data.Application/Dto/Report/AuditHistoryResponse.cs b/services/Data/src/Data.Application/Dto/Report/AuditHistoryResponse.cs new file mode 100644 index 00000000..fffefa1f --- /dev/null +++ b/services/Data/src/Data.Application/Dto/Report/AuditHistoryResponse.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; + +namespace Data.Application.Dto.Report; + +/// Port of the items returned by list_audit_history in report_loader.py. +public sealed class AuditHistoryItem +{ + [JsonPropertyName("reportId")] public long ReportId { get; set; } + [JsonPropertyName("canonicalDomain")] public string? CanonicalDomain { get; set; } + [JsonPropertyName("siteName")] public string? SiteName { get; set; } + [JsonPropertyName("generatedAt")] public string GeneratedAt { get; set; } = ""; + [JsonPropertyName("healthScore")] public int? HealthScore { get; set; } + [JsonPropertyName("categoryScores")] public Dictionary CategoryScores { get; set; } = []; + [JsonPropertyName("issueCounts")] public Dictionary IssueCounts { get; set; } = []; + [JsonPropertyName("perfScore")] public int? PerfScore { get; set; } + [JsonPropertyName("seoScore")] public int? SeoScore { get; set; } + [JsonPropertyName("technicalSeoScore")] public int? TechnicalSeoScore { get; set; } +} + +public sealed class AuditHistoryResponse +{ + [JsonPropertyName("history")] public IReadOnlyList History { get; set; } = []; +} diff --git a/services/Data/src/Data.Application/Dto/Report/MobileDeltaResponse.cs b/services/Data/src/Data.Application/Dto/Report/MobileDeltaResponse.cs new file mode 100644 index 00000000..7f4d47b4 --- /dev/null +++ b/services/Data/src/Data.Application/Dto/Report/MobileDeltaResponse.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; + +namespace Data.Application.Dto.Report; + +/// Per-URL desktop/mobile snapshot from get_mobile_desktop_delta. +public sealed class CrawlPageSnapshot +{ + [JsonPropertyName("title")] public string Title { get; set; } = ""; + [JsonPropertyName("h1")] public string H1 { get; set; } = ""; + [JsonPropertyName("word_count")] public int WordCount { get; set; } + [JsonPropertyName("status")] public int Status { get; set; } +} + +public sealed class MobileDeltaItem +{ + [JsonPropertyName("url")] public string Url { get; set; } = ""; + [JsonPropertyName("desktop")] public CrawlPageSnapshot Desktop { get; set; } = new(); + [JsonPropertyName("mobile")] public CrawlPageSnapshot Mobile { get; set; } = new(); + [JsonPropertyName("title_differs")] public bool TitleDiffers { get; set; } + [JsonPropertyName("h1_differs")] public bool H1Differs { get; set; } + [JsonPropertyName("word_count_delta")] public int WordCountDelta { get; set; } + [JsonPropertyName("status_differs")] public bool StatusDiffers { get; set; } +} + +public sealed class MobileDeltaResponse +{ + [JsonPropertyName("deltas")] public IReadOnlyList Deltas { get; set; } = []; +} diff --git a/services/Data/src/Data.Application/Issues/IssueStatusFingerprint.cs b/services/Data/src/Data.Application/Issues/IssueStatusFingerprint.cs new file mode 100644 index 00000000..7f669b49 --- /dev/null +++ b/services/Data/src/Data.Application/Issues/IssueStatusFingerprint.cs @@ -0,0 +1,17 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Data.Application.Issues; + +/// +/// Port of issue_status_store.issue_fingerprint. +/// +public static class IssueStatusFingerprint +{ + public static string Compute(string message, string url, string? categoryId) + { + var raw = $"{categoryId ?? string.Empty}|{url ?? string.Empty}|{message ?? string.Empty}"; + var hash = SHA256.HashData(Encoding.UTF8.GetBytes(raw)); + return Convert.ToHexString(hash).ToLowerInvariant()[..32]; + } +} diff --git a/services/Data/src/Data.Application/Json/PyIso.cs b/services/Data/src/Data.Application/Json/PyIso.cs new file mode 100644 index 00000000..d0c38ba9 --- /dev/null +++ b/services/Data/src/Data.Application/Json/PyIso.cs @@ -0,0 +1,28 @@ +using System.Globalization; + +namespace Data.Application.Json; + +/// +/// Formats timestamps to match Python's datetime.isoformat() on the UTC values returned by +/// psycopg for TIMESTAMPTZ columns. Rules: offset is rendered as +00:00; the fractional +/// part is OMITTED entirely when microseconds are zero, otherwise rendered as exactly 6 digits +/// (Python keeps trailing zeros and never truncates to milliseconds). +/// +/// +/// Assumes the Postgres session timezone is UTC (the postgres:16-alpine default), so both +/// psycopg and Npgsql surface the same instant at offset 0. Parity is anchored by golden fixtures +/// captured from FastAPI against the same database. +/// +public static class PyIso +{ + public static string Format(DateTimeOffset value) + { + var utc = value.ToUniversalTime(); + var basePart = utc.ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture); + var microseconds = (utc.Ticks % TimeSpan.TicksPerSecond) / 10; // 100ns ticks → microseconds + var frac = microseconds == 0 + ? string.Empty + : "." + microseconds.ToString("D6", CultureInfo.InvariantCulture); + return $"{basePart}{frac}+00:00"; + } +} diff --git a/services/Data/src/Data.Application/Options/DatabaseOptions.cs b/services/Data/src/Data.Application/Options/DatabaseOptions.cs new file mode 100644 index 00000000..90262018 --- /dev/null +++ b/services/Data/src/Data.Application/Options/DatabaseOptions.cs @@ -0,0 +1,23 @@ +namespace Data.Application.Options; + +/// +/// Postgres connection settings for the read-only Data service. is +/// the libpq URI from env DATABASE_URL (the same value the Python services use); it is +/// converted to an Npgsql keyword connection string by NpgsqlDsn.ToNpgsql. +/// +public sealed class DatabaseOptions +{ + public const string SectionName = "Database"; + + /// libpq URI or Npgsql keyword string (env override: DATABASE_URL). + public string ConnectionString { get; set; } = ""; + + /// Minimum pooled connections — mirrors the Python psycopg pool (DB_POOL_MIN, default 2). + public int MinPoolSize { get; set; } = 2; + + /// Maximum pooled connections — mirrors the Python psycopg pool (DB_POOL_MAX, default 20). + public int MaxPoolSize { get; set; } = 20; + + /// Per-query command timeout (seconds). + public int CommandTimeoutSeconds { get; set; } = 30; +} diff --git a/services/Data/src/Data.Application/Persistence/DataDbContext.cs b/services/Data/src/Data.Application/Persistence/DataDbContext.cs new file mode 100644 index 00000000..1546a071 --- /dev/null +++ b/services/Data/src/Data.Application/Persistence/DataDbContext.cs @@ -0,0 +1,90 @@ +using Data.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Data.Application.Persistence; + +/// +/// Read-only EF Core context over the Alembic-owned schema. It NEVER creates or migrates tables: +/// there is no Microsoft.EntityFrameworkCore.Design reference and no Migrations/ folder, +/// and Migrate()/EnsureCreated() are never called. Tracking is disabled globally. +/// +public sealed class DataDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet ReportPayloads => Set(); + + public DbSet CrawlRuns => Set(); + + public DbSet CrawlResults => Set(); + + public DbSet IssueStatuses => Set(); + + public DbSet SavedCrawlFilters => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(e => + { + e.ToTable("report_payload"); + e.HasKey(x => x.Id); + e.Property(x => x.Id).HasColumnName("id"); + e.Property(x => x.GeneratedAt).HasColumnName("generated_at"); + e.Property(x => x.SiteName).HasColumnName("site_name"); + e.Property(x => x.CanonicalDomain).HasColumnName("canonical_domain"); + e.Property(x => x.Data).HasColumnName("data").HasColumnType("jsonb"); + }); + + modelBuilder.Entity(e => + { + e.ToTable("crawl_runs"); + e.HasKey(x => x.Id); + e.Property(x => x.Id).HasColumnName("id"); + e.Property(x => x.CreatedAt).HasColumnName("created_at"); + e.Property(x => x.StartUrl).HasColumnName("start_url"); + e.Property(x => x.RenderMode).HasColumnName("render_mode"); + e.Property(x => x.DiscoveryMode).HasColumnName("discovery_mode"); + e.Property(x => x.MobileRunId).HasColumnName("mobile_run_id"); + }); + + modelBuilder.Entity(e => + { + e.ToTable("crawl_results"); + e.HasKey(x => x.Id); + e.Property(x => x.Id).HasColumnName("id"); + e.Property(x => x.CrawlRunId).HasColumnName("crawl_run_id"); + e.Property(x => x.Url).HasColumnName("url"); + e.Property(x => x.Status).HasColumnName("status"); + e.Property(x => x.Title).HasColumnName("title"); + e.Property(x => x.Data).HasColumnName("data").HasColumnType("jsonb"); + }); + + modelBuilder.Entity(e => + { + e.ToTable("issue_status"); + e.HasKey(x => x.Id); + e.Property(x => x.Id).HasColumnName("id"); + e.Property(x => x.PropertyId).HasColumnName("property_id"); + e.Property(x => x.ReportId).HasColumnName("report_id"); + e.Property(x => x.IssueFingerprint).HasColumnName("issue_fingerprint"); + e.Property(x => x.CategoryId).HasColumnName("category_id"); + e.Property(x => x.Message).HasColumnName("message"); + e.Property(x => x.Url).HasColumnName("url"); + e.Property(x => x.Priority).HasColumnName("priority"); + e.Property(x => x.Status).HasColumnName("status"); + e.Property(x => x.Assignee).HasColumnName("assignee"); + e.Property(x => x.Note).HasColumnName("note"); + e.Property(x => x.CreatedAt).HasColumnName("created_at"); + e.Property(x => x.UpdatedAt).HasColumnName("updated_at"); + }); + + modelBuilder.Entity(e => + { + e.ToTable("saved_crawl_filters"); + e.HasKey(x => x.Id); + e.Property(x => x.Id).HasColumnName("id"); + e.Property(x => x.PropertyId).HasColumnName("property_id"); + e.Property(x => x.Name).HasColumnName("name"); + e.Property(x => x.FilterJson).HasColumnName("filter_json").HasColumnType("jsonb"); + e.Property(x => x.CreatedAt).HasColumnName("created_at"); + }); + } +} diff --git a/services/Data/src/Data.Application/Persistence/NpgsqlDsn.cs b/services/Data/src/Data.Application/Persistence/NpgsqlDsn.cs new file mode 100644 index 00000000..a437ea5e --- /dev/null +++ b/services/Data/src/Data.Application/Persistence/NpgsqlDsn.cs @@ -0,0 +1,91 @@ +using Npgsql; + +namespace Data.Application.Persistence; + +/// +/// Converts a libpq connection URI (postgres://user:pass@host:port/db?connect_timeout=3) — the +/// form used by DATABASE_URL across the Python services — into an Npgsql keyword connection +/// string. Npgsql's parser does NOT accept the postgres:// URI form and throws on it, so this +/// conversion is mandatory. A string that is already in keyword form is passed through unchanged. +/// +public static class NpgsqlDsn +{ + public static string ToNpgsql(string raw) + { + if (string.IsNullOrWhiteSpace(raw)) + { + throw new InvalidOperationException( + "DATABASE_URL is not set. Example: postgres://user:pass@host:5432/website_profiling"); + } + + var s = raw.Trim(); + var isUri = s.StartsWith("postgres://", StringComparison.OrdinalIgnoreCase) + || s.StartsWith("postgresql://", StringComparison.OrdinalIgnoreCase); + if (!isUri) + { + return s; // already an Npgsql keyword connection string + } + + var uri = new Uri(s); + var b = new NpgsqlConnectionStringBuilder + { + Host = Uri.UnescapeDataString(uri.Host), + Port = uri.IsDefaultPort || uri.Port <= 0 ? 5432 : uri.Port, + Database = Uri.UnescapeDataString(uri.AbsolutePath.TrimStart('/')), + }; + + var userInfo = uri.UserInfo.Split(':', 2); + if (userInfo.Length > 0 && userInfo[0].Length > 0) + { + b.Username = Uri.UnescapeDataString(userInfo[0]); + } + if (userInfo.Length > 1) + { + b.Password = Uri.UnescapeDataString(userInfo[1]); + } + + // Translate the query params we understand; strip the rest (Npgsql throws on unknown keywords). + foreach (var (key, value) in ParseQuery(uri.Query)) + { + switch (key.ToLowerInvariant()) + { + case "connect_timeout": + if (int.TryParse(value, out var t)) + { + b.Timeout = t; // Npgsql "Timeout" == connect timeout (seconds) + } + break; + case "sslmode": + if (Enum.TryParse(value, ignoreCase: true, out var mode)) + { + b.SslMode = mode; + } + break; + case "application_name": + b.ApplicationName = value; + break; + // unknown params are intentionally dropped + } + } + + return b.ConnectionString; + } + + private static IEnumerable> ParseQuery(string query) + { + var q = query.TrimStart('?'); + if (q.Length == 0) + { + yield break; + } + foreach (var part in q.Split('&', StringSplitOptions.RemoveEmptyEntries)) + { + var idx = part.IndexOf('='); + yield return idx < 0 + ? new KeyValuePair(Uri.UnescapeDataString(part), "") + : new KeyValuePair( + Uri.UnescapeDataString(part[..idx]), + Uri.UnescapeDataString(part[(idx + 1)..])); + } + } +} diff --git a/services/Data/src/Data.Application/Portfolio/PortfolioConstants.cs b/services/Data/src/Data.Application/Portfolio/PortfolioConstants.cs new file mode 100644 index 00000000..be611b99 --- /dev/null +++ b/services/Data/src/Data.Application/Portfolio/PortfolioConstants.cs @@ -0,0 +1,32 @@ +namespace Data.Application.Portfolio; + +public static class PortfolioConstants +{ + public const int MaxCrawlRuns = 120; + public static readonly TimeSpan GroupsCacheTtl = TimeSpan.FromSeconds(45); + + public const string UnknownBrand = "Unknown property"; + public const string EmDash = "—"; + + public static readonly IReadOnlyList CategoryOrder = + [ + "technical_seo", + "performance", + "core_web_vitals", + "link_health", + "security", + "html_accessibility", + "mobile", + "intelligence", + ]; + + public static readonly HashSet DataSourceIds = new(StringComparer.Ordinal) + { + "crawl", "lighthouse", "search_console", "analytics", "backlinks", + }; + + public static readonly HashSet ValidWidgets = new(StringComparer.OrdinalIgnoreCase) + { + "full", "groups", "summary", "card", + }; +} diff --git a/services/Data/src/Data.Application/Portfolio/PortfolioGrouping.cs b/services/Data/src/Data.Application/Portfolio/PortfolioGrouping.cs new file mode 100644 index 00000000..14853433 --- /dev/null +++ b/services/Data/src/Data.Application/Portfolio/PortfolioGrouping.cs @@ -0,0 +1,426 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Data.Application.Dto.Portfolio; + +namespace Data.Application.Portfolio; + +internal static class PortfolioGrouping +{ + public static IReadOnlyList ComputeDomainGroups( + IReadOnlyList reportList, + PortfolioMaps maps, + Func getPayload) + { + var brandMap = new Dictionary(StringComparer.Ordinal); + + foreach (var r in reportList) + { + var payload = getPayload(r.Id); + if (payload is not { } p) continue; + + var built = BuildReportGroup(r, p, maps); + if (brandMap.TryGetValue(built.BrandKey, out var existing)) + { + if (built.GeneratedAtMs <= existing.GeneratedAtMs) continue; + } + + brandMap[built.BrandKey] = built.Group; + } + + return brandMap.Values.OrderByDescending(g => g.GeneratedAtMs).ToList(); + } + + public static IReadOnlyList ComputeCrawlOnlyGroups( + IReadOnlyList crawlSummaries, + IReadOnlyList reportGroups) + { + var coveredDomains = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var g in reportGroups) + { + var key = (FirstNonEmpty(g.DomainParam, PortfolioHelpers.ExtractHostname(g.CrawlUrl), g.DomainName)).ToLowerInvariant(); + if (!string.IsNullOrEmpty(key)) coveredDomains.Add(key); + } + + var coveredRunIds = reportGroups + .Where(g => g.CrawlRunId is not null) + .Select(g => g.CrawlRunId!.Value) + .ToHashSet(); + + var brandMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var row in crawlSummaries) + { + if (coveredRunIds.Contains(row.CrawlRunId)) continue; + + var startUrl = row.StartUrl.Trim(); + var domainName = PortfolioHelpers.ExtractHostname(startUrl); + if (string.IsNullOrEmpty(domainName)) domainName = PortfolioConstants.UnknownBrand; + var domainKey = domainName.ToLowerInvariant(); + if (string.IsNullOrEmpty(domainKey) || coveredDomains.Contains(domainKey)) continue; + + var urlCount = row.UrlCount; + var titleCoverage = PortfolioHelpers.TitleCoveragePct(row.WithTitle, urlCount); + var generatedAtMs = PortfolioHelpers.GeneratedAtMs(row.CreatedAt); + + if (brandMap.TryGetValue(domainKey, out var existing) && generatedAtMs <= existing.GeneratedAtMs) + continue; + + brandMap[domainKey] = new PortfolioGroupDto + { + DomainName = domainName, + CrawlUrl = string.IsNullOrEmpty(startUrl) ? PortfolioConstants.EmDash : startUrl, + UrlCount = urlCount, + HealthScore = titleCoverage, + StatusCounts = new PortfolioStatusCountsDto + { + S2xx = row.S2xx, + S3xx = row.S3xx, + S4xx = row.S4xx, + S5xx = row.S5xx, + Other = row.Other, + }, + LastCrawl = PortfolioHelpers.ToDisplayDateTime(row.CreatedAt), + LastAudit = "", + TotalIssues = 0, + IssueCounts = EmptyIssueCounts(), + SuccessRate = null, + TitleCoverage = titleCoverage, + AvgWordCount = row.AvgWordCount, + ThinPages = row.ThinPages, + TechnicalSeoScore = null, + PerfScore = null, + SeoScore = null, + CrawlDurationS = null, + CategorySnapshots = [], + SeoSignals = null, + SecurityFindings = 0, + DuplicateClusters = 0, + MedianWordCount = row.AvgWordCount > 0 ? row.AvgWordCount : null, + MedianResponseMs = null, + ReportId = null, + CrawlRunId = row.CrawlRunId, + CrawlOnly = true, + GeneratedAtMs = generatedAtMs, + DomainParam = domainKey, + CrawlConfig = PortfolioHelpers.BuildCrawlConfigFromSummary( + row.RenderMode, row.DiscoveryMode, urlCount), + }; + } + + return brandMap.Values.ToList(); + } + + public static IReadOnlyList MergeGroups( + IReadOnlyList reportGroups, + IReadOnlyList crawlOnlyGroups) => + reportGroups.Concat(crawlOnlyGroups).OrderByDescending(g => g.GeneratedAtMs).ToList(); + + public static PortfolioSummaryResponseDto ComputeSummary(IReadOnlyList groups) + { + var totalBrands = groups.Count; + var totalUrls = groups.Sum(g => g.UrlCount); + int? avgHealth = totalBrands > 0 + ? (int)Math.Round(groups.Sum(g => g.HealthScore) / (double)totalBrands, MidpointRounding.ToEven) + : null; + + return new PortfolioSummaryResponseDto + { + TotalBrands = totalBrands, + TotalUrls = totalUrls, + AvgHealth = avgHealth, + }; + } + + private static (string BrandKey, PortfolioGroupDto Group, double GeneratedAtMs) BuildReportGroup( + PortfolioReportRow r, + JsonElement payload, + PortfolioMaps maps) + { + long? runIdInt = null; + if (payload.TryGetProperty("crawl_run_id", out var ridEl) && ridEl.ValueKind == JsonValueKind.Number) + runIdInt = ridEl.TryGetInt64(out var l) ? l : ridEl.GetInt32(); + + var runStartUrl = runIdInt is not null && maps.StartUrlByRunId.TryGetValue(runIdInt.Value, out var su) + ? su : ""; + var fallbackUrl = PortfolioHelpers.FirstUrlFromPagesOrLinks(payload); + var crawlUrl = (runStartUrl.Length > 0 ? runStartUrl : fallbackUrl).Trim(); + var startDomain = PortfolioHelpers.ExtractHostname(runStartUrl); + var fallbackDomain = PortfolioHelpers.ExtractHostname(crawlUrl); + var domainName = FirstNonEmpty(startDomain, fallbackDomain, + PortfolioHelpers.GetString(payload, "site_name", PortfolioConstants.UnknownBrand)); + var brandKey = startDomain.Length > 0 + ? startDomain + : fallbackDomain.Length > 0 ? $"fallback:{fallbackDomain}" : $"report:{r.Id}"; + + var summary = PortfolioHelpers.GetObj(payload, "summary") ?? default; + var statusCounts = new PortfolioStatusCountsDto + { + S2xx = PortfolioHelpers.GetInt(summary, "count_2xx"), + S3xx = PortfolioHelpers.GetInt(summary, "count_3xx"), + S4xx = PortfolioHelpers.GetInt(summary, "count_4xx"), + S5xx = PortfolioHelpers.GetInt(summary, "count_5xx"), + Other = PortfolioHelpers.GetInt(summary, "count_error"), + }; + + var urlCount = PortfolioHelpers.CrawledUrlCount(payload); + var successPct = urlCount > 0 + ? (int)Math.Round(statusCounts.S2xx / (double)urlCount * 100, MidpointRounding.ToEven) + : 0; + var healthScore = ScoreFromCategories(PortfolioHelpers.GetArrayOrEmpty(payload, "categories")) ?? 0; + + var runCreatedAt = runIdInt is not null && maps.RunCreatedAtByRunId.TryGetValue(runIdInt.Value, out var rc) + ? rc : ""; + var lastCrawl = PortfolioHelpers.ToDisplayDateTime( + runCreatedAt.Length > 0 ? runCreatedAt + : PortfolioHelpers.GetString(payload, "crawl_run_created_at").Length > 0 + ? PortfolioHelpers.GetString(payload, "crawl_run_created_at") + : PortfolioHelpers.GetString(payload, "report_generated_at").Length > 0 + ? PortfolioHelpers.GetString(payload, "report_generated_at") + : r.GeneratedAt); + var lastAudit = PortfolioHelpers.ToDisplayDateTime( + PortfolioHelpers.GetString(payload, "report_generated_at").Length > 0 + ? PortfolioHelpers.GetString(payload, "report_generated_at") + : r.GeneratedAt); + var generatedAtMs = PortfolioHelpers.GeneratedAtMs(r.GeneratedAt); + + var (issueCounts, totalIssues) = IssueCountsFromPayload(payload); + var (perfScore, seoScore) = LhScores(payload); + var technicalSeoScore = CategoryScore(payload, "technical_seo"); + + int? successRate = PortfolioHelpers.GetDoubleOrNull(summary, "success_rate") is { } sr + ? (int)Math.Round(sr, MidpointRounding.ToEven) + : urlCount > 0 ? successPct : null; + + int? crawlDurationS = PortfolioHelpers.GetDoubleOrNull(summary, "crawl_time_s") is { } cts + ? (int)Math.Round(cts, MidpointRounding.ToEven) + : null; + + JsonElement? runMetaEl = null; + if (runIdInt is not null && maps.RunMetaByRunId.TryGetValue(runIdInt.Value, out var rm)) + { + runMetaEl = JsonSerializer.SerializeToElement(new { render_mode = rm.RenderMode, discovery_mode = rm.DiscoveryMode }); + } + + var canonicalHost = PortfolioHelpers.CanonicalDomainFromPayload(payload, maps.StartUrlByRunId); + if (string.IsNullOrEmpty(canonicalHost)) + canonicalHost = PortfolioHelpers.SlugifyDomain(PortfolioHelpers.GetString(payload, "site_name")); + + var group = new PortfolioGroupDto + { + DomainName = domainName, + CrawlUrl = string.IsNullOrEmpty(crawlUrl) ? PortfolioConstants.EmDash : crawlUrl, + UrlCount = urlCount, + HealthScore = healthScore, + StatusCounts = statusCounts, + LastCrawl = lastCrawl, + LastAudit = lastAudit, + TotalIssues = totalIssues, + IssueCounts = issueCounts, + SuccessRate = successRate, + TitleCoverage = null, + AvgWordCount = null, + ThinPages = null, + TechnicalSeoScore = technicalSeoScore, + PerfScore = perfScore, + SeoScore = seoScore, + CrawlDurationS = crawlDurationS, + CategorySnapshots = CategorySnapshots(payload), + SeoSignals = SeoSignals(payload), + SecurityFindings = ArrayLength(payload, "security_findings"), + DuplicateClusters = ArrayLength(payload, "content_duplicates"), + MedianWordCount = MedianWordCount(payload), + MedianResponseMs = MedianResponseMs(payload), + ReportId = r.Id, + CrawlRunId = runIdInt, + GeneratedAtMs = generatedAtMs, + DomainParam = canonicalHost, + CrawlConfig = PortfolioHelpers.BuildCrawlConfigFromPayload(payload, runMetaEl), + DataSources = DataSources(payload), + }; + + return (brandKey, group, generatedAtMs); + } + + private static PortfolioIssueCountsDto EmptyIssueCounts() => new(); + + private static int? ScoreFromCategories(JsonElement categories) + { + if (categories.ValueKind != JsonValueKind.Array) return null; + var nums = new List(); + foreach (var cat in categories.EnumerateArray()) + { + if (cat.TryGetProperty("score", out var sc) && sc.ValueKind == JsonValueKind.Number) + nums.Add(sc.GetDouble()); + } + if (nums.Count == 0) return null; + return (int)Math.Round(nums.Sum() / nums.Count, MidpointRounding.ToEven); + } + + private static (PortfolioIssueCountsDto Counts, int Total) IssueCountsFromPayload(JsonElement payload) + { + var counts = EmptyIssueCounts(); + var cats = PortfolioHelpers.GetArrayOrEmpty(payload, "categories"); + if (cats.ValueKind != JsonValueKind.Array) return (counts, 0); + + foreach (var cat in cats.EnumerateArray()) + { + var issues = PortfolioHelpers.GetArrayOrEmpty(cat, "issues"); + if (issues.ValueKind != JsonValueKind.Array) continue; + foreach (var iss in issues.EnumerateArray()) + { + var p = PortfolioHelpers.GetString(iss, "priority", "Medium"); + switch (p) + { + case "Critical": counts.Critical++; break; + case "High": counts.High++; break; + case "Low": counts.Low++; break; + default: counts.Medium++; break; + } + } + } + + return (counts, counts.Critical + counts.High + counts.Medium + counts.Low); + } + + private static int? CategoryScore(JsonElement payload, string catId) + { + var cats = PortfolioHelpers.GetArrayOrEmpty(payload, "categories"); + if (cats.ValueKind != JsonValueKind.Array) return null; + foreach (var cat in cats.EnumerateArray()) + { + if (PortfolioHelpers.GetString(cat, "id") == catId && + cat.TryGetProperty("score", out var sc) && sc.ValueKind == JsonValueKind.Number) + return (int)Math.Round(sc.GetDouble(), MidpointRounding.ToEven); + } + return null; + } + + private static (int? Perf, int? Seo) LhScores(JsonElement payload) + { + if (PortfolioHelpers.GetObj(payload, "lighthouse_summary") is not { ValueKind: JsonValueKind.Object } summary) + return (null, null); + + var mm = summary.TryGetProperty("median_metrics", out var mmEl) ? mmEl : default; + var cs = summary.TryGetProperty("category_scores", out var csEl) ? csEl : default; + + var perfRaw = FirstTruthy(mm, "performance_score") ?? FirstTruthy(cs, "performance"); + var seoRaw = FirstTruthy(mm, "seo_score") ?? FirstTruthy(cs, "seo"); + + return ( + perfRaw is not null ? (int)Math.Round(perfRaw.Value, MidpointRounding.ToEven) : null, + seoRaw is not null ? (int)Math.Round(seoRaw.Value, MidpointRounding.ToEven) : null); + } + + private static double? FirstTruthy(JsonElement obj, string key) + { + if (obj.ValueKind != JsonValueKind.Object || !obj.TryGetProperty(key, out var v) || + v.ValueKind != JsonValueKind.Number) + return null; + var d = v.GetDouble(); + return d != 0 ? d : null; + } + + private static IReadOnlyList CategorySnapshots(JsonElement payload) + { + var cats = PortfolioHelpers.GetArrayOrEmpty(payload, "categories"); + if (cats.ValueKind != JsonValueKind.Array) return []; + + var byId = new Dictionary(StringComparer.Ordinal); + foreach (var cat in cats.EnumerateArray()) + byId[PortfolioHelpers.GetString(cat, "id")] = cat; + + var outList = new List(); + + void Push(string catId) + { + if (!byId.TryGetValue(catId, out var cat) || + !cat.TryGetProperty("score", out var sc) || sc.ValueKind != JsonValueKind.Number) + return; + outList.Add(new PortfolioCategorySnapshotDto + { + Id = catId, + Name = PortfolioHelpers.GetString(cat, "name", catId), + Score = (int)Math.Round(sc.GetDouble(), MidpointRounding.ToEven), + IssueCount = PortfolioHelpers.ArrayLength(PortfolioHelpers.GetArrayOrEmpty(cat, "issues")), + }); + } + + foreach (var catId in PortfolioConstants.CategoryOrder) Push(catId); + foreach (var cat in cats.EnumerateArray()) + { + var catId = PortfolioHelpers.GetString(cat, "id"); + if (string.IsNullOrEmpty(catId) || outList.Any(r => r.Id == catId)) continue; + if (!cat.TryGetProperty("score", out var sc) || sc.ValueKind != JsonValueKind.Number) continue; + outList.Add(new PortfolioCategorySnapshotDto + { + Id = catId, + Name = PortfolioHelpers.GetString(cat, "name", catId), + Score = (int)Math.Round(sc.GetDouble(), MidpointRounding.ToEven), + IssueCount = PortfolioHelpers.ArrayLength(PortfolioHelpers.GetArrayOrEmpty(cat, "issues")), + }); + } + + return outList; + } + + private static PortfolioSeoSignalsDto? SeoSignals(JsonElement payload) + { + if (PortfolioHelpers.GetObj(payload, "seo_health") is not { ValueKind: JsonValueKind.Object } s) + return null; + return new PortfolioSeoSignalsDto + { + MissingTitles = PortfolioHelpers.GetInt(s, "missing_title"), + MissingMetaDesc = PortfolioHelpers.GetInt(s, "missing_meta_desc"), + ThinContent = PortfolioHelpers.GetInt(s, "thin_content"), + H1Issues = PortfolioHelpers.GetInt(s, "h1_zero") + PortfolioHelpers.GetInt(s, "h1_multi"), + }; + } + + private static int? MedianWordCount(JsonElement payload) + { + if (PortfolioHelpers.GetObj(payload, "content_analytics") is not { ValueKind: JsonValueKind.Object } ca) + return null; + if (!ca.TryGetProperty("word_count_stats", out var wcs) || + wcs.ValueKind != JsonValueKind.Object || + !wcs.TryGetProperty("median", out var median) || median.ValueKind != JsonValueKind.Number) + return null; + return (int)Math.Round(median.GetDouble(), MidpointRounding.ToEven); + } + + private static int? MedianResponseMs(JsonElement payload) + { + if (PortfolioHelpers.GetObj(payload, "response_time_stats") is not { ValueKind: JsonValueKind.Object } rts) + return null; + var p50 = PortfolioHelpers.GetDoubleOrNull(rts, "p50"); + return p50 is not null ? (int)Math.Round(p50.Value, MidpointRounding.ToEven) : null; + } + + private static IReadOnlyList? DataSources(JsonElement payload) + { + if (PortfolioHelpers.GetObj(payload, "report_meta") is not { ValueKind: JsonValueKind.Object } meta) + return null; + if (!meta.TryGetProperty("data_sources", out var raw) || raw.ValueKind != JsonValueKind.Array) + return null; + + var outList = new List(); + foreach (var item in raw.EnumerateArray()) + { + if (item.ValueKind != JsonValueKind.String) continue; + var s = item.GetString(); + if (s is not null && PortfolioConstants.DataSourceIds.Contains(s)) + outList.Add(s); + } + + return outList.Count > 0 ? outList : null; + } + + private static int ArrayLength(JsonElement payload, string name) => + PortfolioHelpers.ArrayLength(PortfolioHelpers.GetArrayOrEmpty(payload, name)); + + private static string FirstNonEmpty(params string?[] values) + { + foreach (var v in values) + if (!string.IsNullOrEmpty(v)) return v; + return ""; + } +} diff --git a/services/Data/src/Data.Application/Portfolio/PortfolioHelpers.cs b/services/Data/src/Data.Application/Portfolio/PortfolioHelpers.cs new file mode 100644 index 00000000..85cb56db --- /dev/null +++ b/services/Data/src/Data.Application/Portfolio/PortfolioHelpers.cs @@ -0,0 +1,202 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; + +namespace Data.Application.Portfolio; + +internal static partial class PortfolioHelpers +{ + private static readonly Regex SlugifyRegex = SlugifyPattern(); + + public static string ExtractHostname(string? url) + { + if (string.IsNullOrWhiteSpace(url)) return ""; + try + { + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) + return ""; + return uri.Host.ToLowerInvariant(); + } + catch + { + return ""; + } + } + + public static string SlugifyDomain(string? name) + { + if (string.IsNullOrWhiteSpace(name)) return ""; + return SlugifyRegex.Replace(name.Trim().ToLowerInvariant(), "-").Trim('-'); + } + + public static string ToDisplayDateTime(string? value) + { + if (string.IsNullOrWhiteSpace(value)) return ""; + try + { + var normalized = value.Replace("Z", "+00:00", StringComparison.Ordinal); + if (DateTimeOffset.TryParse(normalized, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dto)) + return dto.ToString("O"); + } + catch { /* fall through */ } + return value; + } + + public static double GeneratedAtMs(string? value) + { + if (string.IsNullOrWhiteSpace(value)) return 0; + try + { + var normalized = value.Replace("Z", "+00:00", StringComparison.Ordinal); + if (DateTimeOffset.TryParse(normalized, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dto)) + return dto.ToUnixTimeMilliseconds(); + } + catch { /* fall through */ } + return 0; + } + + public static int TitleCoveragePct(int withTitle, int urlCount) => + urlCount <= 0 ? 0 : (int)Math.Round(withTitle / (double)urlCount * 100, MidpointRounding.ToEven); + + public static int? RoundScore(double? value) => + value is null ? null : (int)Math.Round(value.Value, MidpointRounding.ToEven); + + public static JsonElement? GetObj(JsonElement root, string name) => + root.ValueKind == JsonValueKind.Object && root.TryGetProperty(name, out var val) ? val : null; + + public static JsonElement GetArrayOrEmpty(JsonElement root, string name) + { + if (root.ValueKind != JsonValueKind.Object) return default; + return root.TryGetProperty(name, out var val) && val.ValueKind == JsonValueKind.Array ? val : default; + } + + public static string GetString(JsonElement el, string name, string fallback = "") + { + if (el.ValueKind != JsonValueKind.Object || !el.TryGetProperty(name, out var val)) return fallback; + return val.ValueKind == JsonValueKind.String ? val.GetString() ?? fallback : fallback; + } + + public static int GetInt(JsonElement el, string name, int fallback = 0) + { + if (el.ValueKind != JsonValueKind.Object || !el.TryGetProperty(name, out var val)) return fallback; + if (val.ValueKind == JsonValueKind.Number && val.TryGetInt32(out var n)) return n; + if (val.ValueKind == JsonValueKind.String && int.TryParse(val.GetString(), out var s)) return s; + return fallback; + } + + public static double? GetDoubleOrNull(JsonElement el, string name) + { + if (el.ValueKind != JsonValueKind.Object || !el.TryGetProperty(name, out var val)) return null; + if (val.ValueKind == JsonValueKind.Number) return val.GetDouble(); + return null; + } + + public static int ArrayLength(JsonElement el) + { + if (el.ValueKind == JsonValueKind.Array) return el.GetArrayLength(); + return 0; + } + + public static string FirstUrlFromPagesOrLinks(JsonElement payload) + { + var top = GetArrayOrEmpty(payload, "top_pages"); + if (top.ValueKind == JsonValueKind.Array && top.GetArrayLength() > 0) + { + var first = top[0]; + if (first.ValueKind == JsonValueKind.Object) + return GetString(first, "url"); + } + + var links = GetArrayOrEmpty(payload, "links"); + if (links.ValueKind == JsonValueKind.Array && links.GetArrayLength() > 0) + { + var first = links[0]; + if (first.ValueKind == JsonValueKind.Object) + return GetString(first, "url"); + } + + return ""; + } + + public static string CanonicalDomainFromPayload(JsonElement payload, IReadOnlyDictionary startUrlByRunId) + { + long? runId = null; + if (payload.TryGetProperty("crawl_run_id", out var rid) && rid.ValueKind == JsonValueKind.Number) + runId = rid.TryGetInt64(out var l) ? l : rid.GetInt32(); + + var runStart = runId is not null && startUrlByRunId.TryGetValue(runId.Value, out var s) ? s : ""; + var fallback = FirstUrlFromPagesOrLinks(payload); + var startDomain = ExtractHostname(runStart); + var fallbackDomain = ExtractHostname(fallback); + return (startDomain.Length > 0 ? startDomain : fallbackDomain).ToLowerInvariant(); + } + + public static int CrawledUrlCount(JsonElement payload) + { + var meta = GetObj(payload, "report_meta"); + if (meta is { } m && m.TryGetProperty("crawl_scope", out var scope) && + scope.ValueKind == JsonValueKind.Object && + scope.TryGetProperty("pages_crawled", out var pages) && pages.ValueKind == JsonValueKind.Number) + { + var n = pages.TryGetInt32(out var i) ? i : (int)pages.GetDouble(); + if (n > 0) return n; + } + + var summary = GetObj(payload, "summary"); + if (summary is { } s) + { + var total = GetDoubleOrNull(s, "total_urls"); + if (total is > 0) return (int)total.Value; + } + + return ArrayLength(GetArrayOrEmpty(payload, "links")); + } + + public static JsonNode? BuildCrawlConfigFromPayload(JsonElement payload, JsonElement? runMeta) + { + JsonObject? cfg = null; + var meta = GetObj(payload, "report_meta"); + if (meta is { } m && m.TryGetProperty("crawl_scope", out var scope) && + scope.ValueKind == JsonValueKind.Object) + { + cfg = JsonNode.Parse(scope.GetRawText()) as JsonObject ?? new JsonObject(); + } + + var hasRunMeta = runMeta is { ValueKind: JsonValueKind.Object } rm && + (rm.TryGetProperty("render_mode", out var rmVal) && rmVal.ValueKind != JsonValueKind.Null || + rm.TryGetProperty("discovery_mode", out var dmVal) && dmVal.ValueKind != JsonValueKind.Null); + + if (cfg is null && !hasRunMeta) return null; + + cfg ??= new JsonObject(); + + if (runMeta is { ValueKind: JsonValueKind.Object } metaEl) + { + if (metaEl.TryGetProperty("render_mode", out var render) && render.ValueKind == JsonValueKind.String && + !cfg.ContainsKey("render_mode")) + cfg["render_mode"] = render.GetString(); + if (metaEl.TryGetProperty("discovery_mode", out var disc) && disc.ValueKind == JsonValueKind.String) + cfg["discovery_mode"] = disc.GetString(); + } + + return cfg.Count > 0 ? cfg : null; + } + + public static JsonNode? BuildCrawlConfigFromSummary( + string? renderMode, string? discoveryMode, int urlCount) + { + if (string.IsNullOrEmpty(renderMode) && string.IsNullOrEmpty(discoveryMode) && urlCount == 0) + return null; + + return new JsonObject + { + ["pages_crawled"] = urlCount, + ["render_mode"] = renderMode, + ["discovery_mode"] = discoveryMode, + }; + } + + [GeneratedRegex("[^a-z0-9]+", RegexOptions.CultureInvariant)] + private static partial Regex SlugifyPattern(); +} diff --git a/services/Data/src/Data.Application/Portfolio/PortfolioHistory.cs b/services/Data/src/Data.Application/Portfolio/PortfolioHistory.cs new file mode 100644 index 00000000..6d340677 --- /dev/null +++ b/services/Data/src/Data.Application/Portfolio/PortfolioHistory.cs @@ -0,0 +1,43 @@ +using Data.Application.Dto.Portfolio; + +namespace Data.Application.Portfolio; + +internal static class PortfolioHistory +{ + public static Dictionary> BuildCrawlHistoryByDomain( + IReadOnlyList summaries) + { + var byDomain = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + foreach (var row in summaries) + { + var key = PortfolioHelpers.ExtractHostname(row.StartUrl); + if (string.IsNullOrEmpty(key)) continue; + + var pages = row.UrlCount; + var point = new PortfolioCrawlHistoryPointDto + { + PagesDiscovered = pages, + TitleCoverage = PortfolioHelpers.TitleCoveragePct(row.WithTitle, pages), + AvgWordCount = row.AvgWordCount, + CreatedAtMs = PortfolioHelpers.GeneratedAtMs(row.CreatedAt), + }; + + if (!byDomain.TryGetValue(key, out var list)) + { + list = []; + byDomain[key] = list; + } + + list.Add(point); + } + + return byDomain.ToDictionary( + kv => kv.Key, + kv => (IReadOnlyList)kv.Value + .OrderBy(p => p.CreatedAtMs) + .TakeLast(8) + .ToList(), + StringComparer.OrdinalIgnoreCase); + } +} diff --git a/services/Data/src/Data.Application/Portfolio/PortfolioMaps.cs b/services/Data/src/Data.Application/Portfolio/PortfolioMaps.cs new file mode 100644 index 00000000..a3920857 --- /dev/null +++ b/services/Data/src/Data.Application/Portfolio/PortfolioMaps.cs @@ -0,0 +1,41 @@ +using Data.Application.Dto.Portfolio; + +namespace Data.Application.Portfolio; + +internal sealed class PortfolioMaps +{ + public required IReadOnlyDictionary StartUrlByRunId { get; init; } + public required IReadOnlyDictionary RunCreatedAtByRunId { get; init; } + public required IReadOnlyDictionary RunMetaByRunId { get; init; } + public required IReadOnlyList CrawlSummaries { get; init; } + + public sealed class CrawlRunMeta + { + public string? RenderMode { get; init; } + public string? DiscoveryMode { get; init; } + } + + public static PortfolioMaps Load( + IReadOnlyList crawlRows, + IReadOnlyList crawlSummaries) + { + var startUrl = new Dictionary(); + var createdAt = new Dictionary(); + var meta = new Dictionary(); + + foreach (var row in crawlRows) + { + startUrl[row.Id] = row.StartUrl; + createdAt[row.Id] = row.CreatedAt; + meta[row.Id] = new CrawlRunMeta { RenderMode = row.RenderMode, DiscoveryMode = row.DiscoveryMode }; + } + + return new PortfolioMaps + { + StartUrlByRunId = startUrl, + RunCreatedAtByRunId = createdAt, + RunMetaByRunId = meta, + CrawlSummaries = crawlSummaries, + }; + } +} diff --git a/services/Data/src/Data.Application/Portfolio/PortfolioService.cs b/services/Data/src/Data.Application/Portfolio/PortfolioService.cs new file mode 100644 index 00000000..e09c77b9 --- /dev/null +++ b/services/Data/src/Data.Application/Portfolio/PortfolioService.cs @@ -0,0 +1,177 @@ +using System.Text.Json; +using Data.Application.Dto.Portfolio; +using Data.Application.Repositories; +using Microsoft.Extensions.Caching.Memory; + +namespace Data.Application.Portfolio; + +public interface IPortfolioService +{ + Task GetPortfolioResponseAsync( + string widget, + IReadOnlyList ids, + long? reportId, + long? crawlRunId, + CancellationToken cancellationToken); +} + +public sealed class PortfolioService( + IPortfolioRepository repository, + IMemoryCache cache) : IPortfolioService +{ + public async Task GetPortfolioResponseAsync( + string widget, + IReadOnlyList ids, + long? reportId, + long? crawlRunId, + CancellationToken cancellationToken) + { + if (widget.Equals("groups", StringComparison.OrdinalIgnoreCase)) + { + var keyParts = await repository.GetCacheKeyPartsAsync(cancellationToken); + var cacheKey = $"portfolio:groups:{keyParts.ReportCount}:{keyParts.ReportMaxId}:{keyParts.CrawlCount}:{keyParts.CrawlMaxId}"; + if (cache.TryGetValue(cacheKey, out PortfolioGroupsResponseDto? cached) && cached is not null) + return cached; + } + + var allReports = await repository.ListReportsAsync(cancellationToken); + var idSet = ids.ToHashSet(); + IReadOnlyList reportList; + + if ((widget.Equals("groups", StringComparison.OrdinalIgnoreCase) || + widget.Equals("summary", StringComparison.OrdinalIgnoreCase)) && ids.Count == 0) + { + reportList = await repository.ListReportsLatestPerDomainAsync(cancellationToken); + } + else if (ids.Count > 0) + { + reportList = allReports.Where(r => idSet.Contains(r.Id)).ToList(); + } + else + { + reportList = allReports; + } + + if (widget.Equals("card", StringComparison.OrdinalIgnoreCase)) + { + var crawlRows = await repository.ListCrawlRunsAsync(cancellationToken); + var summaries = await repository.ListCrawlRunSummariesAsync(null, cancellationToken); + var maps = PortfolioMaps.Load(crawlRows, summaries); + var group = await BuildPortfolioCardAsync( + reportList, maps, reportId, crawlRunId, cancellationToken); + return new PortfolioCardResponseDto { Group = group }; + } + + var bundle = await BuildGroupsBundleAsync(reportList, cancellationToken); + + if (widget.Equals("summary", StringComparison.OrdinalIgnoreCase)) + return PortfolioGrouping.ComputeSummary(bundle.Groups); + + var payload = new PortfolioGroupsResponseDto + { + Groups = bundle.Groups, + CrawlHistoryByDomain = bundle.CrawlHistoryByDomain, + }; + + if (widget.Equals("groups", StringComparison.OrdinalIgnoreCase)) + { + var keyParts = await repository.GetCacheKeyPartsAsync(cancellationToken); + var cacheKey = $"portfolio:groups:{keyParts.ReportCount}:{keyParts.ReportMaxId}:{keyParts.CrawlCount}:{keyParts.CrawlMaxId}"; + cache.Set(cacheKey, payload, PortfolioConstants.GroupsCacheTtl); + } + + return payload; + } + + private async Task BuildGroupsBundleAsync( + IReadOnlyList reportList, + CancellationToken cancellationToken) + { + var crawlRows = await repository.ListCrawlRunsAsync(cancellationToken); + var summaries = await repository.ListCrawlRunSummariesAsync( + PortfolioConstants.MaxCrawlRuns, cancellationToken); + var maps = PortfolioMaps.Load(crawlRows, summaries); + + var reportIds = reportList.Select(r => r.Id).ToList(); + var payloadJson = await repository.ReadReportPayloadsPortfolioAsync(reportIds, cancellationToken); + + JsonElement? GetPayload(long rid) + { + if (!payloadJson.TryGetValue(rid, out var json)) return null; + try + { + using var doc = JsonDocument.Parse(json); + return doc.RootElement.Clone(); + } + catch + { + return null; + } + } + + var reportGroups = PortfolioGrouping.ComputeDomainGroups(reportList, maps, GetPayload); + var crawlOnly = PortfolioGrouping.ComputeCrawlOnlyGroups(maps.CrawlSummaries, reportGroups); + var groups = PortfolioGrouping.MergeGroups(reportGroups, crawlOnly); + var crawlHistory = PortfolioHistory.BuildCrawlHistoryByDomain(maps.CrawlSummaries); + + return new PortfolioGroupsResponseDto + { + Groups = groups, + CrawlHistoryByDomain = crawlHistory, + }; + } + + private async Task BuildPortfolioCardAsync( + IReadOnlyList reportList, + PortfolioMaps maps, + long? reportId, + long? crawlRunId, + CancellationToken cancellationToken) + { + static JsonElement? ParsePayload(string? json) + { + if (json is null) return null; + try + { + using var doc = JsonDocument.Parse(json); + return doc.RootElement.Clone(); + } + catch + { + return null; + } + } + + if (reportId is not null) + { + var row = reportList.FirstOrDefault(r => r.Id == reportId.Value); + if (row is null) return null; + var json = await repository.ReadReportPayloadAsync(reportId.Value, cancellationToken); + var payload = ParsePayload(json); + var groups = PortfolioGrouping.ComputeDomainGroups([row], maps, _ => payload); + return groups.FirstOrDefault(); + } + + if (crawlRunId is not null) + { + var matchedId = await repository.FindReportIdByCrawlRunIdAsync(crawlRunId.Value, cancellationToken); + if (matchedId is not null) + { + var row = reportList.FirstOrDefault(r => r.Id == matchedId.Value) + ?? new PortfolioReportRow { Id = matchedId.Value }; + var json = await repository.ReadReportPayloadAsync(matchedId.Value, cancellationToken); + var payload = ParsePayload(json); + var groups = PortfolioGrouping.ComputeDomainGroups([row], maps, _ => payload); + var fromReport = groups.FirstOrDefault(); + if (fromReport is not null) return fromReport; + } + + var summary = maps.CrawlSummaries.FirstOrDefault(s => s.CrawlRunId == crawlRunId.Value); + if (summary is null) return null; + var crawlOnly = PortfolioGrouping.ComputeCrawlOnlyGroups([summary], []); + return crawlOnly.FirstOrDefault(); + } + + return null; + } +} diff --git a/services/Data/src/Data.Application/Report/SectionFields.cs b/services/Data/src/Data.Application/Report/SectionFields.cs new file mode 100644 index 00000000..1ded44c9 --- /dev/null +++ b/services/Data/src/Data.Application/Report/SectionFields.cs @@ -0,0 +1,61 @@ +namespace Data.Application.Report; + +/// +/// Port of SECTION_FIELDS from report_loader.py. +/// Maps section key → the subset of top-level JSON fields the client wants. +/// +public static class SectionFields +{ + public static readonly IReadOnlyDictionary> ByKey = + new Dictionary>(StringComparer.Ordinal) + { + ["core"] = + [ + "site_name", "summary", "categories", "top_pages", "recommendations", + "seo_health", "social_coverage", "status_counts", "portfolio_benchmark", + "executive_summary", "crux_summary", "report_meta", "report_generated_at", + "crawl_only_preview", "crawl_run_id", "crawl_run_created_at", "site_level", + "ml_errors", + ], + ["links"] = + [ + "links", "link_edges", "link_rel_summary", "inlink_anchor_matrix", + "outbound_link_domains", "outlink_labels", "outlink_counts", + ], + ["traffic"] = ["google"], + ["keywords"] = + [ + "keywords", "keyword_opportunities", "competitor_keyword_gap", + "semantic_keyword_clusters", + ], + ["issues"] = ["issues", "redirects"], + ["content"] = + [ + "content_urls", "content_duplicates", "content_analytics", + "text_content_analysis", "response_time_stats", + ], + ["lighthouse"] = + [ + "lighthouse_summary", "lighthouse_by_url", "lighthouse_diagnostics", + "lighthouse_human_summary", + ], + ["security"] = ["security_findings"], + ["gsc-links"] = ["gsc_links", "bing_backlinks"], + ["structure"] = ["graph_nodes", "graph_edges", "depth_distribution"], + ["tech"] = ["tech_stack_summary", "subdomains", "contact_intelligence"], + ["indexation"] = + [ + "indexation_coverage", "hreflang_summary", "ner_site_summary", + "language_summary", "rich_results_validation", "url_fingerprints", + "rich_results_meta", + ], + ["gallery"] = + [ + "mime_labels", "mime_values", "title_labels", "title_counts", + "domain_labels", "domain_values", + ], + }; + + public static readonly IReadOnlySet ValidKeys = + new HashSet(ByKey.Keys, StringComparer.Ordinal); +} diff --git a/services/Data/src/Data.Application/Repositories/IIssueStatusRepository.cs b/services/Data/src/Data.Application/Repositories/IIssueStatusRepository.cs new file mode 100644 index 00000000..0f57e3dc --- /dev/null +++ b/services/Data/src/Data.Application/Repositories/IIssueStatusRepository.cs @@ -0,0 +1,10 @@ +using Data.Application.Dto.Issues; + +namespace Data.Application.Repositories; + +public interface IIssueStatusRepository +{ + Task> ListAsync(int propertyId, CancellationToken cancellationToken); + + Task UpsertAsync(UpsertIssueStatusRequest request, CancellationToken cancellationToken); +} diff --git a/services/Data/src/Data.Application/Repositories/IPortfolioRepository.cs b/services/Data/src/Data.Application/Repositories/IPortfolioRepository.cs new file mode 100644 index 00000000..58963035 --- /dev/null +++ b/services/Data/src/Data.Application/Repositories/IPortfolioRepository.cs @@ -0,0 +1,31 @@ +using Data.Application.Dto.Portfolio; + +namespace Data.Application.Repositories; + +public interface IPortfolioRepository +{ + Task<(int ReportCount, long ReportMaxId, int CrawlCount, long CrawlMaxId)> GetCacheKeyPartsAsync( + CancellationToken cancellationToken); + + Task> ListReportsAsync(CancellationToken cancellationToken); + + Task> ListReportsLatestPerDomainAsync(CancellationToken cancellationToken); + + Task> ListCrawlRunsAsync(CancellationToken cancellationToken); + + Task> ListCrawlRunSummariesAsync( + int? maxRuns, CancellationToken cancellationToken); + + Task> ReadReportPayloadsPortfolioAsync( + IReadOnlyList reportIds, CancellationToken cancellationToken); + + Task ReadReportPayloadAsync(long reportId, CancellationToken cancellationToken); + + Task FindReportIdByCrawlRunIdAsync(long crawlRunId, CancellationToken cancellationToken); + + /// + /// Port of portfolio_store.delete_portfolio_item: deletes report and/or crawl run rows. + /// Returns true if at least one delete succeeded (when both ids are given, the last op wins, matching Python). + /// + Task DeletePortfolioItemAsync(long? reportId, long? crawlRunId, CancellationToken cancellationToken); +} diff --git a/services/Data/src/Data.Application/Repositories/IReportRepository.cs b/services/Data/src/Data.Application/Repositories/IReportRepository.cs new file mode 100644 index 00000000..827047ec --- /dev/null +++ b/services/Data/src/Data.Application/Repositories/IReportRepository.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Nodes; +using Data.Application.Dto.Meta; +using Data.Application.Dto.Report; + +namespace Data.Application.Repositories; + +public interface IReportRepository +{ + /// Port of GET /api/report/meta: report list + crawl-run list. + Task GetMetaAsync(CancellationToken cancellationToken); + + /// + /// Port of get_report_payload: fetches the raw data JSONB column, + /// resolving by or by domain match. Returns null if not found. + /// + Task GetPayloadDataAsync(long? reportId, string? domain, CancellationToken ct); + + /// + /// Port of list_audit_history: ordered by generated_at DESC, optional domain filter + /// (exact lower-case or slugified regexp_replace match). propertyId is not supported + /// (report_payload has no property_id column; the Python filter would SQL-error if called). + /// + Task ListAuditHistoryAsync(string? domain, int limit, CancellationToken ct); + + /// + /// Port of get_crawl_preview_payload: returns the crawl run header + all crawl_results + /// rows merged as top_pages. Returns null if the crawl run is not found. + /// + Task GetCrawlPreviewPayloadAsync(long crawlRunId, CancellationToken ct); + + /// + /// Port of get_mobile_desktop_delta: compares desktop vs mobile crawl results and + /// returns only URLs where something differs. Returns empty list if no mobile_run_id. + /// + Task GetMobileDeltaAsync(long runId, CancellationToken ct); +} diff --git a/services/Data/src/Data.Application/Repositories/ISavedFilterRepository.cs b/services/Data/src/Data.Application/Repositories/ISavedFilterRepository.cs new file mode 100644 index 00000000..24043789 --- /dev/null +++ b/services/Data/src/Data.Application/Repositories/ISavedFilterRepository.cs @@ -0,0 +1,13 @@ +using System.Text.Json; +using Data.Application.Dto.Filters; + +namespace Data.Application.Repositories; + +public interface ISavedFilterRepository +{ + Task> ListAsync(int propertyId, CancellationToken cancellationToken); + + Task UpsertAsync(long propertyId, string name, JsonElement filterJson, CancellationToken cancellationToken); + + Task DeleteAsync(long propertyId, string name, CancellationToken cancellationToken); +} diff --git a/services/Data/src/Data.Application/Repositories/IssueStatusRepository.cs b/services/Data/src/Data.Application/Repositories/IssueStatusRepository.cs new file mode 100644 index 00000000..60c3ecb8 --- /dev/null +++ b/services/Data/src/Data.Application/Repositories/IssueStatusRepository.cs @@ -0,0 +1,123 @@ +using Data.Application.Dto.Issues; +using Data.Application.Issues; +using Data.Application.Json; +using Data.Application.Persistence; +using Microsoft.EntityFrameworkCore; +using Npgsql; + +namespace Data.Application.Repositories; + +public sealed class IssueStatusRepository(DataDbContext db, NpgsqlDataSource dataSource) : IIssueStatusRepository +{ + private static readonly HashSet ValidStatuses = + new(StringComparer.Ordinal) { "open", "in_progress", "fixed", "ignored" }; + + private const string SelectColumns = """ + id, property_id, report_id, issue_fingerprint, category_id, + message, url, priority, status, assignee, note, updated_at + """; + + private const string UpsertSql = $""" + INSERT INTO issue_status + (property_id, report_id, issue_fingerprint, category_id, message, url, + priority, status, assignee, note, updated_at) + VALUES (@property_id, @report_id, @issue_fingerprint, @category_id, @message, @url, + @priority, @status, @assignee, @note, now()) + ON CONFLICT (property_id, issue_fingerprint) DO UPDATE SET + status = EXCLUDED.status, + assignee = COALESCE(EXCLUDED.assignee, issue_status.assignee), + note = COALESCE(EXCLUDED.note, issue_status.note), + report_id = COALESCE(EXCLUDED.report_id, issue_status.report_id), + updated_at = now() + RETURNING {SelectColumns} + """; + + public async Task> ListAsync( + int propertyId, CancellationToken cancellationToken) + { + var rows = await db.Set() + .AsNoTracking() + .Where(x => x.PropertyId == propertyId) + .OrderByDescending(x => x.UpdatedAt) + .ToListAsync(cancellationToken); + + return rows.Select(MapEntity).ToList(); + } + + public async Task UpsertAsync( + UpsertIssueStatusRequest request, CancellationToken cancellationToken) + { + var status = request.Status ?? string.Empty; + if (!ValidStatuses.Contains(status)) + throw new ArgumentException($"invalid status: {status}"); + + var message = request.Message ?? string.Empty; + var url = request.Url ?? string.Empty; + var priority = request.Priority ?? "Medium"; + var fingerprint = IssueStatusFingerprint.Compute(message, url, request.CategoryId); + + await using var conn = await dataSource.OpenConnectionAsync(cancellationToken); + await using var cmd = new NpgsqlCommand(UpsertSql, conn); + cmd.Parameters.AddWithValue("property_id", request.PropertyId); + cmd.Parameters.AddWithValue("report_id", (object?)request.ReportId ?? DBNull.Value); + cmd.Parameters.AddWithValue("issue_fingerprint", fingerprint); + cmd.Parameters.AddWithValue("category_id", (object?)request.CategoryId ?? DBNull.Value); + cmd.Parameters.AddWithValue("message", message); + cmd.Parameters.AddWithValue("url", url); + cmd.Parameters.AddWithValue("priority", priority); + cmd.Parameters.AddWithValue("status", status); + cmd.Parameters.AddWithValue("assignee", (object?)request.Assignee ?? DBNull.Value); + cmd.Parameters.AddWithValue("note", (object?)request.Note ?? DBNull.Value); + + await using var reader = await cmd.ExecuteReaderAsync(cancellationToken); + if (!await reader.ReadAsync(cancellationToken)) + throw new InvalidOperationException("issue status upsert failed"); + + return MapReader(reader); + } + + private static IssueStatusRowDto MapEntity(Data.Domain.Entities.IssueStatus row) => new() + { + Id = row.Id, + PropertyId = row.PropertyId, + ReportId = row.ReportId, + IssueFingerprint = row.IssueFingerprint, + CategoryId = row.CategoryId, + Message = row.Message, + Url = row.Url, + Priority = row.Priority, + Status = row.Status, + Assignee = row.Assignee, + Note = row.Note, + UpdatedAt = PyIso.Format(row.UpdatedAt), + }; + + private static IssueStatusRowDto MapReader(NpgsqlDataReader reader) + { + var updatedAt = reader.GetFieldValue(reader.GetOrdinal("updated_at")); + var reportOrdinal = reader.GetOrdinal("report_id"); + long? reportId = reader.IsDBNull(reportOrdinal) ? null : reader.GetInt64(reportOrdinal); + + return new IssueStatusRowDto + { + Id = reader.GetInt64(reader.GetOrdinal("id")), + PropertyId = reader.GetInt64(reader.GetOrdinal("property_id")), + ReportId = reportId, + IssueFingerprint = reader.GetString(reader.GetOrdinal("issue_fingerprint")), + CategoryId = reader.IsDBNull(reader.GetOrdinal("category_id")) + ? null + : reader.GetString(reader.GetOrdinal("category_id")), + Message = reader.GetString(reader.GetOrdinal("message")), + Url = reader.GetString(reader.GetOrdinal("url")), + Priority = reader.GetString(reader.GetOrdinal("priority")), + Status = reader.GetString(reader.GetOrdinal("status")), + Assignee = reader.IsDBNull(reader.GetOrdinal("assignee")) + ? null + : reader.GetString(reader.GetOrdinal("assignee")), + Note = reader.IsDBNull(reader.GetOrdinal("note")) + ? null + : reader.GetString(reader.GetOrdinal("note")), + UpdatedAt = PyIso.Format(updatedAt), + }; + } +} diff --git a/services/Data/src/Data.Application/Repositories/PortfolioRepository.cs b/services/Data/src/Data.Application/Repositories/PortfolioRepository.cs new file mode 100644 index 00000000..e8bb4c0b --- /dev/null +++ b/services/Data/src/Data.Application/Repositories/PortfolioRepository.cs @@ -0,0 +1,305 @@ +using Data.Application.Dto.Portfolio; +using Data.Application.Json; +using Data.Application.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Npgsql; + +namespace Data.Application.Repositories; + +public sealed class PortfolioRepository( + DataDbContext db, + NpgsqlDataSource dataSource, + ILogger logger) : IPortfolioRepository +{ + private const string PortfolioPayloadSql = """ + SELECT id, + jsonb_build_object( + 'site_name', data->'site_name', + 'summary', data->'summary', + 'categories', data->'categories', + 'top_pages', COALESCE(data->'top_pages', '[]'::jsonb), + 'report_meta', data->'report_meta', + 'report_generated_at', data->'report_generated_at', + 'crawl_run_id', data->'crawl_run_id', + 'crawl_run_created_at', data->'crawl_run_created_at', + 'lighthouse_summary', jsonb_build_object( + 'median_metrics', data->'lighthouse_summary'->'median_metrics', + 'category_scores', data->'lighthouse_summary'->'category_scores' + ), + 'seo_health', data->'seo_health', + 'content_analytics', jsonb_build_object( + 'word_count_stats', data->'content_analytics'->'word_count_stats' + ), + 'response_time_stats', jsonb_build_object( + 'p50', data->'response_time_stats'->'p50' + ), + 'security_findings', COALESCE(data->'security_findings', '[]'::jsonb), + 'content_duplicates', COALESCE(data->'content_duplicates', '[]'::jsonb) + ) AS data + FROM report_payload + WHERE id = ANY(@ids) + """; + + public async Task<(int ReportCount, long ReportMaxId, int CrawlCount, long CrawlMaxId)> GetCacheKeyPartsAsync( + CancellationToken cancellationToken) + { + try + { + await using var conn = await dataSource.OpenConnectionAsync(cancellationToken); + await using var cmd = new NpgsqlCommand( + """ + SELECT + (SELECT COUNT(*)::int FROM report_payload), + (SELECT COALESCE(MAX(id), 0)::bigint FROM report_payload), + (SELECT COUNT(*)::int FROM crawl_runs), + (SELECT COALESCE(MAX(id), 0)::bigint FROM crawl_runs) + """, + conn); + await using var reader = await cmd.ExecuteReaderAsync(cancellationToken); + if (!await reader.ReadAsync(cancellationToken)) + return (0, 0, 0, 0); + return ( + reader.GetInt32(0), + reader.GetInt64(1), + reader.GetInt32(2), + reader.GetInt64(3)); + } + catch (Exception ex) + { + logger.LogWarning(ex, "portfolio cache key query failed"); + return (0, 0, 0, 0); + } + } + + public async Task> ListReportsAsync(CancellationToken cancellationToken) + { + var rows = await db.ReportPayloads + .OrderByDescending(r => r.Id) + .Select(r => new { r.Id, r.CanonicalDomain, r.SiteName, r.GeneratedAt }) + .ToListAsync(cancellationToken); + + return rows.Select(r => MapReportRow(r.Id, r.CanonicalDomain, r.SiteName, r.GeneratedAt)).ToList(); + } + + public async Task> ListReportsLatestPerDomainAsync( + CancellationToken cancellationToken) + { + var entities = await db.ReportPayloads + .FromSql($""" + SELECT id, canonical_domain, site_name, generated_at, data + FROM ( + SELECT DISTINCT ON (COALESCE(NULLIF(canonical_domain, ''), site_name)) + id, canonical_domain, site_name, generated_at, data + FROM report_payload + ORDER BY COALESCE(NULLIF(canonical_domain, ''), site_name), generated_at DESC + ) latest + """) + .ToListAsync(cancellationToken); + + return entities.Select(e => MapReportRow(e.Id, e.CanonicalDomain, e.SiteName, e.GeneratedAt)).ToList(); + } + + public async Task> ListCrawlRunsAsync(CancellationToken cancellationToken) + { + try + { + var rows = await db.CrawlRuns + .OrderByDescending(c => c.Id) + .Select(c => new { c.Id, c.StartUrl, c.CreatedAt, c.RenderMode, c.DiscoveryMode }) + .ToListAsync(cancellationToken); + + return rows.Select(c => new PortfolioCrawlRunRow + { + Id = c.Id, + StartUrl = c.StartUrl ?? "", + CreatedAt = PyIso.Format(c.CreatedAt), + RenderMode = c.RenderMode, + DiscoveryMode = c.DiscoveryMode, + }).ToList(); + } + catch (Exception ex) + { + logger.LogWarning(ex, "list_crawl_runs failed; returning empty list"); + return []; + } + } + + public async Task> ListCrawlRunSummariesAsync( + int? maxRuns, CancellationToken cancellationToken) + { + try + { + var runFilter = ""; + if (maxRuns is > 0) + { + runFilter = """ + WHERE cr.id IN ( + SELECT id FROM crawl_runs ORDER BY id DESC LIMIT @maxRuns + ) + """; + } + + var sql = $""" + SELECT + cr.id AS crawl_run_id, + cr.start_url, + cr.created_at, + cr.render_mode, + cr.discovery_mode, + COUNT(crl.id)::int AS url_count, + COUNT(*) FILTER (WHERE crl.status LIKE '2%')::int AS s2xx, + COUNT(*) FILTER (WHERE crl.status LIKE '3%')::int AS s3xx, + COUNT(*) FILTER (WHERE crl.status LIKE '4%')::int AS s4xx, + COUNT(*) FILTER (WHERE crl.status LIKE '5%')::int AS s5xx, + COUNT(*) FILTER ( + WHERE crl.status IS NULL + OR crl.status = '' + OR crl.status !~ '^[2345]' + )::int AS other, + COUNT(*) FILTER ( + WHERE NULLIF(TRIM(COALESCE(crl.title, crl.data->>'title', '')), '') IS NOT NULL + )::int AS with_title, + COALESCE(ROUND(AVG(NULLIF((crl.data->>'word_count')::numeric, 0))), 0)::int AS avg_word_count, + COUNT(*) FILTER ( + WHERE COALESCE((crl.data->>'word_count')::int, 0) > 0 + AND COALESCE((crl.data->>'word_count')::int, 0) < 300 + )::int AS thin_pages + FROM crawl_runs cr + LEFT JOIN crawl_results crl ON crl.crawl_run_id = cr.id + {runFilter} + GROUP BY cr.id, cr.start_url, cr.created_at, cr.render_mode, cr.discovery_mode + ORDER BY cr.id DESC + """; + + await using var conn = await dataSource.OpenConnectionAsync(cancellationToken); + await using var cmd = new NpgsqlCommand(sql, conn); + if (maxRuns is > 0) + cmd.Parameters.AddWithValue("maxRuns", maxRuns.Value); + + var result = new List(); + await using var reader = await cmd.ExecuteReaderAsync(cancellationToken); + while (await reader.ReadAsync(cancellationToken)) + { + var created = reader.GetFieldValue(2); + result.Add(new PortfolioCrawlSummaryRow + { + CrawlRunId = reader.GetInt64(0), + StartUrl = reader.IsDBNull(1) ? "" : reader.GetString(1), + CreatedAt = PyIso.Format(created), + RenderMode = reader.IsDBNull(3) ? null : reader.GetString(3), + DiscoveryMode = reader.IsDBNull(4) ? null : reader.GetString(4), + UrlCount = reader.GetInt32(5), + S2xx = reader.GetInt32(6), + S3xx = reader.GetInt32(7), + S4xx = reader.GetInt32(8), + S5xx = reader.GetInt32(9), + Other = reader.GetInt32(10), + WithTitle = reader.GetInt32(11), + AvgWordCount = reader.GetInt32(12), + ThinPages = reader.GetInt32(13), + }); + } + + return result; + } + catch (Exception ex) + { + logger.LogWarning(ex, "list_crawl_run_summaries failed; returning empty list"); + return []; + } + } + + public async Task> ReadReportPayloadsPortfolioAsync( + IReadOnlyList reportIds, CancellationToken cancellationToken) + { + if (reportIds.Count == 0) + return new Dictionary(); + + try + { + await using var conn = await dataSource.OpenConnectionAsync(cancellationToken); + await using var cmd = new NpgsqlCommand(PortfolioPayloadSql, conn); + cmd.Parameters.AddWithValue("ids", reportIds.ToArray()); + + var outMap = new Dictionary(); + await using var reader = await cmd.ExecuteReaderAsync(cancellationToken); + while (await reader.ReadAsync(cancellationToken)) + { + var id = reader.GetInt64(0); + var json = reader.GetString(1); + outMap[id] = json; + } + + return outMap; + } + catch (Exception ex) + { + logger.LogWarning(ex, "read_report_payloads_portfolio failed"); + return new Dictionary(); + } + } + + public async Task ReadReportPayloadAsync(long reportId, CancellationToken cancellationToken) => + await db.ReportPayloads + .Where(r => r.Id == reportId) + .Select(r => r.Data) + .FirstOrDefaultAsync(cancellationToken); + + public async Task FindReportIdByCrawlRunIdAsync(long crawlRunId, CancellationToken cancellationToken) + { + try + { + await using var conn = await dataSource.OpenConnectionAsync(cancellationToken); + await using var cmd = new NpgsqlCommand( + """ + SELECT id FROM report_payload + WHERE (data->>'crawl_run_id')::bigint = @crawlRunId + ORDER BY id DESC + LIMIT 1 + """, + conn); + cmd.Parameters.AddWithValue("crawlRunId", crawlRunId); + var result = await cmd.ExecuteScalarAsync(cancellationToken); + return result is long id ? id : result is int i ? i : null; + } + catch (Exception ex) + { + logger.LogWarning(ex, "find report by crawl_run_id failed"); + return null; + } + } + + public async Task DeletePortfolioItemAsync( + long? reportId, long? crawlRunId, CancellationToken cancellationToken) + { + var deleted = false; + if (reportId is not null) + { + var count = await db.ReportPayloads + .Where(r => r.Id == reportId.Value) + .ExecuteDeleteAsync(cancellationToken); + deleted = count > 0; + } + + if (crawlRunId is not null) + { + var count = await db.CrawlRuns + .Where(c => c.Id == crawlRunId.Value) + .ExecuteDeleteAsync(cancellationToken); + deleted = count > 0; + } + + return deleted; + } + + private static PortfolioReportRow MapReportRow( + long id, string? canonicalDomain, string? siteName, DateTimeOffset generatedAt) => + new() + { + Id = id, + CanonicalDomain = canonicalDomain, + SiteName = siteName, + GeneratedAt = PyIso.Format(generatedAt), + }; +} diff --git a/services/Data/src/Data.Application/Repositories/ReportRepository.cs b/services/Data/src/Data.Application/Repositories/ReportRepository.cs new file mode 100644 index 00000000..59ecede8 --- /dev/null +++ b/services/Data/src/Data.Application/Repositories/ReportRepository.cs @@ -0,0 +1,406 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Data.Application.Dto.Meta; +using Data.Application.Dto.Report; +using Data.Application.Json; +using Data.Application.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Data.Application.Repositories; + +public sealed class ReportRepository(DataDbContext db, ILogger logger) : IReportRepository +{ + // ── /api/report/meta ──────────────────────────────────────────────────── + + public async Task GetMetaAsync(CancellationToken cancellationToken) + { + return new ReportMetaResponse + { + Reports = await ListReportsAsync(cancellationToken), + CrawlRuns = await ListCrawlRunsAsync(cancellationToken), + }; + } + + private async Task> ListReportsAsync(CancellationToken ct) + { + var rows = await db.ReportPayloads + .OrderByDescending(r => r.Id) + .Select(r => new { r.Id, r.CanonicalDomain, r.SiteName, r.GeneratedAt }) + .ToListAsync(ct); + + return rows.Select(r => new ReportListItem + { + Id = r.Id, + CanonicalDomain = r.CanonicalDomain, + SiteName = r.SiteName, + GeneratedAt = PyIso.Format(r.GeneratedAt), + }).ToList(); + } + + private async Task> ListCrawlRunsAsync(CancellationToken ct) + { + try + { + var rows = await db.CrawlRuns + .OrderByDescending(c => c.Id) + .Select(c => new { c.Id, c.StartUrl, c.CreatedAt, c.RenderMode, c.DiscoveryMode }) + .ToListAsync(ct); + + return rows.Select(c => new CrawlRunItem + { + Id = c.Id, + StartUrl = c.StartUrl ?? "", + CreatedAt = PyIso.Format(c.CreatedAt), + RenderMode = c.RenderMode, + DiscoveryMode = c.DiscoveryMode, + }).ToList(); + } + catch (Exception ex) + { + logger.LogWarning(ex, "list_crawl_runs query failed; returning empty list"); + return []; + } + } + + // ── /api/report/payload ────────────────────────────────────────────────── + + public async Task GetPayloadDataAsync(long? reportId, string? domain, CancellationToken ct) + { + long? resolvedId = reportId; + + if (resolvedId is null && !string.IsNullOrWhiteSpace(domain)) + { + var domainLower = domain.Trim().ToLowerInvariant(); + resolvedId = await db.ReportPayloads + .Where(r => r.CanonicalDomain != null && + r.CanonicalDomain.ToLower() == domainLower) + .OrderByDescending(r => r.Id) + .Select(r => (long?)r.Id) + .FirstOrDefaultAsync(ct); + } + + if (resolvedId is not null) + { + return await db.ReportPayloads + .Where(r => r.Id == resolvedId.Value) + .Select(r => r.Data) + .FirstOrDefaultAsync(ct); + } + + // No id or domain → latest report (mirrors Python read_report_payload(conn, None)) + return await db.ReportPayloads + .OrderByDescending(r => r.Id) + .Select(r => r.Data) + .FirstOrDefaultAsync(ct); + } + + // ── /api/report/history ────────────────────────────────────────────────── + + public async Task ListAuditHistoryAsync( + string? domain, int limit, CancellationToken ct) + { + limit = Math.Clamp(limit, 1, 100); + List<(long Id, string? CanonicalDomain, string? SiteName, DateTimeOffset GeneratedAt, string Data)> rows; + + if (!string.IsNullOrWhiteSpace(domain)) + { + var normalized = domain.Trim().ToLowerInvariant(); + // Mirror the Python regexp_replace domain match; EF FromSql safely parameterises values. + var entities = await db.ReportPayloads + .FromSql($""" + SELECT id, canonical_domain, site_name, generated_at, data + FROM report_payload + WHERE LOWER(canonical_domain) = {normalized} + OR regexp_replace(LOWER(COALESCE(canonical_domain, '')), '[^a-z0-9]+', '-', 'g') = {normalized} + ORDER BY generated_at DESC + LIMIT {limit} + """) + .ToListAsync(ct); + rows = entities.Select(e => (e.Id, e.CanonicalDomain, e.SiteName, e.GeneratedAt, e.Data)).ToList(); + } + else + { + var entities = await db.ReportPayloads + .OrderByDescending(r => r.GeneratedAt) + .Take(limit) + .Select(r => new { r.Id, r.CanonicalDomain, r.SiteName, r.GeneratedAt, r.Data }) + .ToListAsync(ct); + rows = entities.Select(e => (e.Id, e.CanonicalDomain, e.SiteName, e.GeneratedAt, e.Data)).ToList(); + } + + return new AuditHistoryResponse + { + History = rows.Select(MapHistoryItem).ToList(), + }; + } + + private static AuditHistoryItem MapHistoryItem( + (long Id, string? CanonicalDomain, string? SiteName, DateTimeOffset GeneratedAt, string Data) row) + { + JsonElement data = default; + try + { + using var doc = JsonDocument.Parse(row.Data); + data = doc.RootElement.Clone(); + } + catch { /* corrupt JSON → proceed with defaults */ } + + var categories = data.ValueKind == JsonValueKind.Object && + data.TryGetProperty("categories", out var cats) && + cats.ValueKind == JsonValueKind.Array + ? cats + : default; + + var categoryScores = new Dictionary(); + var issueCounts = new Dictionary(4) + { ["Critical"] = 0, ["High"] = 0, ["Medium"] = 0, ["Low"] = 0 }; + + if (categories.ValueKind == JsonValueKind.Array) + { + foreach (var cat in categories.EnumerateArray()) + { + // categoryScores: {id or name or "unknown": score} + var keyElem = cat.TryGetProperty("id", out var id) ? id + : cat.TryGetProperty("name", out var nm) ? nm + : default; + var catKey = keyElem.ValueKind == JsonValueKind.String + ? keyElem.GetString() ?? "unknown" + : "unknown"; + if (cat.TryGetProperty("score", out var sc) && sc.ValueKind == JsonValueKind.Number) + categoryScores[catKey] = sc.GetDouble(); + + // issueCounts + if (cat.TryGetProperty("issues", out var issues) && issues.ValueKind == JsonValueKind.Array) + { + foreach (var issue in issues.EnumerateArray()) + { + var priority = issue.TryGetProperty("priority", out var p) && + p.ValueKind == JsonValueKind.String + ? p.GetString() ?? "Medium" + : "Medium"; + issueCounts[priority] = issueCounts.GetValueOrDefault(priority) + 1; + } + } + } + } + + return new AuditHistoryItem + { + ReportId = row.Id, + CanonicalDomain = row.CanonicalDomain, + SiteName = row.SiteName, + GeneratedAt = PyIso.Format(row.GeneratedAt), + HealthScore = AvgScore(categories), + CategoryScores = categoryScores, + IssueCounts = issueCounts, + PerfScore = LhScore(data, "performance_score", "performance"), + SeoScore = LhScore(data, "seo_score", "seo"), + TechnicalSeoScore = TechSeoScore(categories), + }; + } + + // round(sum/len) with banker's rounding — mirrors Python's builtin round(). + private static int? AvgScore(JsonElement categories) + { + if (categories.ValueKind != JsonValueKind.Array) return null; + var nums = new List(); + foreach (var cat in categories.EnumerateArray()) + if (cat.TryGetProperty("score", out var s) && s.ValueKind == JsonValueKind.Number) + nums.Add(s.GetDouble()); + if (nums.Count == 0) return null; + return (int)Math.Round(nums.Sum() / nums.Count, MidpointRounding.ToEven); + } + + // Mirrors _lh_scores: try median_metrics.{mmKey} first (non-zero), then category_scores.{csKey}. + private static int? LhScore(JsonElement data, string mmKey, string csKey) + { + if (data.ValueKind != JsonValueKind.Object || + !data.TryGetProperty("lighthouse_summary", out var summary) || + summary.ValueKind != JsonValueKind.Object) + return null; + + summary.TryGetProperty("median_metrics", out var mm); + summary.TryGetProperty("category_scores", out var cs); + + var raw = FirstTruthy(mm, mmKey) ?? FirstTruthy(cs, csKey); + return raw is null ? null : (int)Math.Round(raw.Value, MidpointRounding.ToEven); + } + + // Returns value if it exists in the object AND is a non-zero number (mirrors Python `or` truthiness). + private static double? FirstTruthy(JsonElement obj, string key) + { + if (obj.ValueKind != JsonValueKind.Object) return null; + if (!obj.TryGetProperty(key, out var v) || v.ValueKind != JsonValueKind.Number) return null; + var d = v.GetDouble(); + return d != 0 ? d : null; + } + + private static int? TechSeoScore(JsonElement categories) + { + if (categories.ValueKind != JsonValueKind.Array) return null; + foreach (var cat in categories.EnumerateArray()) + { + if (cat.TryGetProperty("id", out var id) && id.GetString() == "technical_seo") + { + if (cat.TryGetProperty("score", out var s) && s.ValueKind == JsonValueKind.Number) + return (int)Math.Round(s.GetDouble(), MidpointRounding.ToEven); + break; + } + } + return null; + } + + // ── /api/report/crawl-payload ──────────────────────────────────────────── + + public async Task GetCrawlPreviewPayloadAsync(long crawlRunId, CancellationToken ct) + { + var run = await db.CrawlRuns + .Where(c => c.Id == crawlRunId) + .Select(c => new { c.Id, c.StartUrl }) + .FirstOrDefaultAsync(ct); + + if (run is null) return null; + + string siteHost = ""; + if (!string.IsNullOrEmpty(run.StartUrl)) + { + try { siteHost = new Uri(run.StartUrl).Host; } + catch { /* invalid URL → leave empty */ } + } + + var results = await db.CrawlResults + .Where(r => r.CrawlRunId == crawlRunId) + .Select(r => new { r.Url, r.Data }) + .ToListAsync(ct); + + var pages = new JsonArray(); + foreach (var result in results) + { + var pageObj = new JsonObject(); + // Set url first; then spread data on top — mirrors Python {"url": url, **data}. + pageObj["url"] = result.Url ?? ""; + if (!string.IsNullOrEmpty(result.Data)) + { + try + { + using var dataDoc = JsonDocument.Parse(result.Data); + if (dataDoc.RootElement.ValueKind == JsonValueKind.Object) + { + foreach (var prop in dataDoc.RootElement.EnumerateObject()) + pageObj[prop.Name] = JsonNode.Parse(prop.Value.GetRawText()); + } + } + catch { /* corrupt JSON → keep url-only object */ } + } + pages.Add(pageObj); + } + + return new JsonObject + { + ["crawl_only_preview"] = true, + ["crawl_run_id"] = crawlRunId, + ["site_name"] = siteHost, + ["top_pages"] = pages, + }; + } + + // ── /api/report/mobile-delta ───────────────────────────────────────────── + + public async Task GetMobileDeltaAsync(long runId, CancellationToken ct) + { + var run = await db.CrawlRuns + .Where(c => c.Id == runId) + .Select(c => new { c.MobileRunId }) + .FirstOrDefaultAsync(ct); + + if (run?.MobileRunId is null) + return new MobileDeltaResponse { Deltas = [] }; + + var mobileRunId = run.MobileRunId.Value; + + var desktopMap = await FetchRunMapAsync(runId, ct); + var mobileMap = await FetchRunMapAsync(mobileRunId, ct); + + var deltas = new List(); + foreach (var (key, desktop) in desktopMap) + { + if (!mobileMap.TryGetValue(key, out var mobile)) continue; + + bool titleDiffers = desktop.Title != mobile.Title; + bool h1Differs = desktop.H1 != mobile.H1; + int wcDelta = Math.Abs(desktop.WordCount - mobile.WordCount); + bool statusDiffers = desktop.Status != mobile.Status; + + if (!titleDiffers && !h1Differs && wcDelta <= 50 && !statusDiffers) continue; + + deltas.Add(new MobileDeltaItem + { + Url = key, + Desktop = desktop, + Mobile = mobile, + TitleDiffers = titleDiffers, + H1Differs = h1Differs, + WordCountDelta = wcDelta, + StatusDiffers = statusDiffers, + }); + } + + // Stable sort descending by (status*4 + title*2 + h1) — mirrors Python's sort(key=..., reverse=True). + var sorted = deltas + .OrderByDescending(d => + (d.StatusDiffers ? 4 : 0) + (d.TitleDiffers ? 2 : 0) + (d.H1Differs ? 1 : 0)) + .ToList(); + + return new MobileDeltaResponse { Deltas = sorted }; + } + + private async Task> FetchRunMapAsync( + long runId, CancellationToken ct) + { + var rows = await db.CrawlResults + .Where(r => r.CrawlRunId == runId) + .Select(r => new { r.Url, r.Data }) + .ToListAsync(ct); + + var map = new Dictionary(StringComparer.Ordinal); + foreach (var row in rows) + { + // Normalize key the same way Python does: rstrip('/') + lower(). + var key = (row.Url ?? "").TrimEnd('/').ToLowerInvariant(); + + JsonElement data = default; + try + { + using var doc = JsonDocument.Parse(row.Data); + data = doc.RootElement.Clone(); + } + catch { /* corrupt JSONB → use defaults */ } + + map[key] = new CrawlPageSnapshot + { + Title = StringFromJson(data, "title"), + H1 = StringFromJson(data, "h1"), + WordCount = IntFromJson(data, "word_count"), + Status = IntFromJson(data, "status"), + }; + } + return map; + } + + private static string StringFromJson(JsonElement el, string key) + { + if (el.ValueKind != JsonValueKind.Object) return ""; + if (!el.TryGetProperty(key, out var v)) return ""; + return v.ValueKind == JsonValueKind.String ? v.GetString() ?? "" : ""; + } + + private static int IntFromJson(JsonElement el, string key) + { + if (el.ValueKind != JsonValueKind.Object) return 0; + if (!el.TryGetProperty(key, out var v)) return 0; + if (v.ValueKind == JsonValueKind.Number) return v.TryGetInt32(out var n) ? n : (int)v.GetDouble(); + if (v.ValueKind == JsonValueKind.String && int.TryParse(v.GetString(), out var s)) return s; + return 0; + } +} diff --git a/services/Data/src/Data.Application/Repositories/SavedFilterRepository.cs b/services/Data/src/Data.Application/Repositories/SavedFilterRepository.cs new file mode 100644 index 00000000..8b9b77cb --- /dev/null +++ b/services/Data/src/Data.Application/Repositories/SavedFilterRepository.cs @@ -0,0 +1,74 @@ +using System.Text.Json; +using Data.Application.Dto.Filters; +using Data.Application.Json; +using Data.Application.Persistence; +using Microsoft.EntityFrameworkCore; +using Npgsql; +using NpgsqlTypes; + +namespace Data.Application.Repositories; + +public sealed class SavedFilterRepository(DataDbContext db, NpgsqlDataSource dataSource) : ISavedFilterRepository +{ + private const string UpsertSql = """ + INSERT INTO saved_crawl_filters (property_id, name, filter_json) + VALUES (@property_id, @name, @filter_json) + ON CONFLICT (property_id, name) DO UPDATE SET filter_json = EXCLUDED.filter_json + """; + + public async Task> ListAsync( + int propertyId, CancellationToken cancellationToken) + { + var rows = await db.Set() + .AsNoTracking() + .Where(x => x.PropertyId == propertyId) + .OrderBy(x => x.Name) + .ToListAsync(cancellationToken); + + return rows.Select(MapEntity).ToList(); + } + + public async Task UpsertAsync( + long propertyId, string name, JsonElement filterJson, CancellationToken cancellationToken) + { + await using var conn = await dataSource.OpenConnectionAsync(cancellationToken); + await using var cmd = new NpgsqlCommand(UpsertSql, conn); + cmd.Parameters.AddWithValue("property_id", propertyId); + cmd.Parameters.AddWithValue("name", name); + cmd.Parameters.Add(new NpgsqlParameter("filter_json", NpgsqlDbType.Jsonb) + { + Value = filterJson.GetRawText(), + }); + await cmd.ExecuteNonQueryAsync(cancellationToken); + } + + public async Task DeleteAsync(long propertyId, string name, CancellationToken cancellationToken) + { + var count = await db.Set() + .Where(x => x.PropertyId == propertyId && x.Name == name) + .ExecuteDeleteAsync(cancellationToken); + return count > 0; + } + + private static SavedFilterRowDto MapEntity(Data.Domain.Entities.SavedCrawlFilter row) + { + JsonElement filterJson; + try + { + filterJson = JsonDocument.Parse(row.FilterJson).RootElement.Clone(); + } + catch (JsonException) + { + filterJson = JsonSerializer.SerializeToElement(new { }); + } + + return new SavedFilterRowDto + { + Id = row.Id, + PropertyId = row.PropertyId, + Name = row.Name, + FilterJson = filterJson, + CreatedAt = PyIso.Format(row.CreatedAt), + }; + } +} diff --git a/services/Data/src/Data.Domain/Data.Domain.csproj b/services/Data/src/Data.Domain/Data.Domain.csproj new file mode 100644 index 00000000..9ed914b5 --- /dev/null +++ b/services/Data/src/Data.Domain/Data.Domain.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/services/Data/src/Data.Domain/Entities/CrawlResult.cs b/services/Data/src/Data.Domain/Entities/CrawlResult.cs new file mode 100644 index 00000000..4ac2a53f --- /dev/null +++ b/services/Data/src/Data.Domain/Entities/CrawlResult.cs @@ -0,0 +1,24 @@ +namespace Data.Domain.Entities; + +/// +/// Read-only mapping of the existing crawl_results table (owned by Alembic migrations). +/// Columns status and title were added in migration 002; data JSONB holds +/// all raw page attributes. The Data service never writes or migrates this table. +/// +public sealed class CrawlResult +{ + public long Id { get; set; } + public long CrawlRunId { get; set; } + + /// url TEXT NOT NULL + public string Url { get; set; } = ""; + + /// status TEXT — HTTP status string, e.g. "200", "404" (added in migration 002). + public string? Status { get; set; } + + /// title TEXT — page title extracted column (added in migration 002). + public string? Title { get; set; } + + /// data JSONB NOT NULL — raw page attributes; parse with System.Text.Json. + public string Data { get; set; } = "{}"; +} diff --git a/services/Data/src/Data.Domain/Entities/CrawlRun.cs b/services/Data/src/Data.Domain/Entities/CrawlRun.cs new file mode 100644 index 00000000..88759d43 --- /dev/null +++ b/services/Data/src/Data.Domain/Entities/CrawlRun.cs @@ -0,0 +1,24 @@ +namespace Data.Domain.Entities; + +/// +/// Read-only mapping of the existing crawl_runs table (owned by Alembic migrations). +/// The Data service never writes or migrates this table. +/// +public sealed class CrawlRun +{ + public long Id { get; set; } + + /// created_at TIMESTAMPTZ → mapped to . + public DateTimeOffset CreatedAt { get; set; } + + public string? StartUrl { get; set; } + + /// render_mode TEXT DEFAULT 'static' (added in migration 008). + public string? RenderMode { get; set; } + + /// discovery_mode TEXT DEFAULT 'spider' (added in migration 013). + public string? DiscoveryMode { get; set; } + + /// mobile_run_id INT (added in migration 019) — paired mobile crawl run, or null. + public long? MobileRunId { get; set; } +} diff --git a/services/Data/src/Data.Domain/Entities/IssueStatus.cs b/services/Data/src/Data.Domain/Entities/IssueStatus.cs new file mode 100644 index 00000000..d4e4fa46 --- /dev/null +++ b/services/Data/src/Data.Domain/Entities/IssueStatus.cs @@ -0,0 +1,33 @@ +namespace Data.Domain.Entities; + +/// +/// Mapping of the Alembic-owned issue_status table (issue workflow on the task board). +/// +public sealed class IssueStatus +{ + public long Id { get; set; } + + public long PropertyId { get; set; } + + public long? ReportId { get; set; } + + public string IssueFingerprint { get; set; } = string.Empty; + + public string? CategoryId { get; set; } + + public string Message { get; set; } = string.Empty; + + public string Url { get; set; } = string.Empty; + + public string Priority { get; set; } = "Medium"; + + public string Status { get; set; } = "open"; + + public string? Assignee { get; set; } + + public string? Note { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public DateTimeOffset UpdatedAt { get; set; } +} diff --git a/services/Data/src/Data.Domain/Entities/ReportPayload.cs b/services/Data/src/Data.Domain/Entities/ReportPayload.cs new file mode 100644 index 00000000..3f3d8525 --- /dev/null +++ b/services/Data/src/Data.Domain/Entities/ReportPayload.cs @@ -0,0 +1,20 @@ +namespace Data.Domain.Entities; + +/// +/// Read-only mapping of the existing report_payload table (owned by Alembic migrations). +/// The Data service never writes or migrates this table. +/// +public sealed class ReportPayload +{ + public long Id { get; set; } + + /// generated_at TIMESTAMPTZ → mapped to . + public DateTimeOffset GeneratedAt { get; set; } + + public string? SiteName { get; set; } + + public string? CanonicalDomain { get; set; } + + /// data JSONB mapped as raw JSON text; parsed with System.Text.Json where needed. + public string Data { get; set; } = "{}"; +} diff --git a/services/Data/src/Data.Domain/Entities/SavedCrawlFilter.cs b/services/Data/src/Data.Domain/Entities/SavedCrawlFilter.cs new file mode 100644 index 00000000..c3b269e9 --- /dev/null +++ b/services/Data/src/Data.Domain/Entities/SavedCrawlFilter.cs @@ -0,0 +1,18 @@ +namespace Data.Domain.Entities; + +/// +/// Mapping of the Alembic-owned saved_crawl_filters table (Links page saved filter presets). +/// +public sealed class SavedCrawlFilter +{ + public long Id { get; set; } + + public long PropertyId { get; set; } + + public string Name { get; set; } = string.Empty; + + /// filter_json JSONB stored as raw JSON text. + public string FilterJson { get; set; } = "{}"; + + public DateTimeOffset CreatedAt { get; set; } +} diff --git a/services/Data/tests/Data.Tests/ApiIntegrationTests.cs b/services/Data/tests/Data.Tests/ApiIntegrationTests.cs new file mode 100644 index 00000000..29837868 --- /dev/null +++ b/services/Data/tests/Data.Tests/ApiIntegrationTests.cs @@ -0,0 +1,606 @@ +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Nodes; +using Data.Application.Dto.Meta; +using Data.Application.Dto.Filters; +using Data.Application.Dto.Issues; +using Data.Application.Dto.Portfolio; +using Data.Application.Dto.Report; +using Data.Application.Portfolio; +using Data.Application.Repositories; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Data.Tests; + +public class ApiIntegrationTests : IClassFixture> +{ + private readonly WebApplicationFactory _factory; + + public ApiIntegrationTests(WebApplicationFactory factory) + { + _factory = factory.WithWebHostBuilder(builder => + { + builder.UseEnvironment("Development"); + builder.ConfigureServices(services => + { + services.RemoveAll(); + services.RemoveAll(); + services.RemoveAll(); + services.RemoveAll(); + services.RemoveAll(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + }); + }); + } + + [Fact] + public async Task Health_returns_ok() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/health"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task Report_meta_returns_expected_json_shape() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/api/report/meta"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + var root = doc.RootElement; + Assert.True(root.TryGetProperty("reports", out var reports)); + Assert.True(root.TryGetProperty("crawlRuns", out _)); + Assert.Equal(JsonValueKind.Array, reports.ValueKind); + Assert.True(reports.GetArrayLength() > 0); + var row = reports[0]; + Assert.True(row.TryGetProperty("canonical_domain", out _)); + Assert.False(row.TryGetProperty("canonicalDomain", out _)); + } + + [Fact] + public async Task Report_payload_full_streams_raw_json() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/api/report/payload?reportId=1"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal("{\"payload\":{\"site_name\":\"example.com\"}}", body); + } + + [Fact] + public async Task Report_payload_invalid_section_returns_400() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/api/report/payload?reportId=1§ion=bad"); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("Invalid section", doc.RootElement.GetProperty("detail").GetString()); + } + + [Fact] + public async Task Report_payload_not_found_returns_404() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/api/report/payload?reportId=999"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("Report not found", doc.RootElement.GetProperty("detail").GetString()); + } + + [Fact] + public async Task Report_history_returns_history_key() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/api/report/history"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.True(doc.RootElement.TryGetProperty("history", out var history)); + Assert.Equal(JsonValueKind.Array, history.ValueKind); + } + + [Fact] + public async Task Report_mobile_delta_requires_id() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/api/report/mobile-delta"); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("id required", doc.RootElement.GetProperty("detail").GetString()); + } + + [Fact] + public async Task Report_portfolio_invalid_widget_returns_400() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/api/report/portfolio?widget=bad"); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("Invalid widget", doc.RootElement.GetProperty("detail").GetString()); + } + + [Fact] + public async Task Report_portfolio_groups_returns_expected_keys() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/api/report/portfolio?widget=groups"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.True(doc.RootElement.TryGetProperty("groups", out var groups)); + Assert.True(doc.RootElement.TryGetProperty("crawlHistoryByDomain", out _)); + Assert.Equal(JsonValueKind.Array, groups.ValueKind); + } + + [Fact] + public async Task Report_portfolio_card_requires_id() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/api/report/portfolio?widget=card"); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("reportId or crawlRunId required for card widget", doc.RootElement.GetProperty("detail").GetString()); + } + + [Fact] + public async Task Report_portfolio_card_returns_group() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/api/report/portfolio?widget=card&reportId=1"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.True(doc.RootElement.TryGetProperty("group", out var group)); + Assert.Equal("example.com", group.GetProperty("domainName").GetString()); + } + + [Fact] + public async Task Swagger_json_lists_report_routes() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/swagger/v1/swagger.json"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var json = await response.Content.ReadAsStringAsync(); + Assert.Contains("Website Profiling Data API", json); + Assert.Contains("/api/report/meta", json); + Assert.Contains("/api/report/portfolio", json); + Assert.Contains("/api/portfolio/delete", json); + Assert.Contains("/api/issues/status", json); + Assert.Contains("/api/filters", json); + } + + [Fact] + public async Task Filters_list_missing_propertyId_returns_400() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/api/filters"); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("propertyId required", doc.RootElement.GetProperty("detail").GetString()); + } + + [Fact] + public async Task Filters_list_zero_propertyId_returns_400() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/api/filters?propertyId=0"); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("propertyId required", doc.RootElement.GetProperty("detail").GetString()); + } + + [Fact] + public async Task Filters_delete_missing_fields_returns_400() + { + var client = _factory.CreateClient(); + var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Delete, "/api/filters") + { + Content = JsonContent.Create(new { propertyId = 1L }), + }); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("propertyId and name required", doc.RootElement.GetProperty("detail").GetString()); + } + + [Fact] + public async Task Filters_list_returns_filters_key() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/api/filters?propertyId=1"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.True(doc.RootElement.TryGetProperty("filters", out var filters)); + Assert.Equal(JsonValueKind.Array, filters.ValueKind); + Assert.Equal(1, filters.GetArrayLength()); + Assert.Equal("status-200", filters[0].GetProperty("name").GetString()); + } + + [Fact] + public async Task Filters_upsert_success_returns_ok() + { + var client = _factory.CreateClient(); + var response = await client.PostAsJsonAsync("/api/filters", new + { + propertyId = 1L, + name = "my-filter", + filterJson = new { status = new[] { "200" } }, + }); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.True(doc.RootElement.GetProperty("ok").GetBoolean()); + } + + [Fact] + public async Task Filters_upsert_missing_fields_returns_400() + { + var client = _factory.CreateClient(); + var response = await client.PostAsJsonAsync("/api/filters", new { propertyId = 1L }); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("propertyId and name required", doc.RootElement.GetProperty("detail").GetString()); + } + + [Fact] + public async Task Filters_delete_not_found_returns_404() + { + var client = _factory.CreateClient(); + var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Delete, "/api/filters") + { + Content = JsonContent.Create(new { propertyId = 1L, name = "missing" }), + }); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("filter not found", doc.RootElement.GetProperty("detail").GetString()); + } + + [Fact] + public async Task Filters_delete_success_returns_ok() + { + var client = _factory.CreateClient(); + var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Delete, "/api/filters") + { + Content = JsonContent.Create(new { propertyId = 1L, name = "status-200" }), + }); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.True(doc.RootElement.GetProperty("ok").GetBoolean()); + } + + private sealed class FakeSavedFilterRepository : ISavedFilterRepository + { + public Task> ListAsync(int propertyId, CancellationToken cancellationToken) => + Task.FromResult>( + [ + new SavedFilterRowDto + { + Id = 1, + PropertyId = propertyId, + Name = "status-200", + FilterJson = JsonSerializer.SerializeToElement(new { status = new[] { "200" } }), + CreatedAt = "2024-01-01T00:00:00+00:00", + }, + ]); + + public Task UpsertAsync( + long propertyId, string name, JsonElement filterJson, CancellationToken cancellationToken) => + Task.CompletedTask; + + public Task DeleteAsync(long propertyId, string name, CancellationToken cancellationToken) => + Task.FromResult(name != "missing"); + } + + [Fact] + public async Task Issues_status_list_returns_issues_key() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/api/issues/status?propertyId=1"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.True(doc.RootElement.TryGetProperty("issues", out var issues)); + Assert.Equal(JsonValueKind.Array, issues.ValueKind); + Assert.Equal(1, issues.GetArrayLength()); + } + + [Fact] + public async Task Issues_status_list_missing_propertyId_returns_400() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/api/issues/status"); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("propertyId required", doc.RootElement.GetProperty("detail").GetString()); + } + + [Fact] + public async Task Issues_status_list_zero_propertyId_returns_400() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/api/issues/status?propertyId=0"); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("propertyId required", doc.RootElement.GetProperty("detail").GetString()); + } + + [Fact] + public async Task Issues_status_upsert_success_returns_issue() + { + var client = _factory.CreateClient(); + var response = await client.PutAsJsonAsync("/api/issues/status", new + { + propertyId = 1L, + message = "Missing meta description", + status = "open", + url = "https://example.com/page", + priority = "Medium", + }); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + var issue = doc.RootElement.GetProperty("issue"); + Assert.Equal(1, issue.GetProperty("propertyId").GetInt64()); + Assert.Equal("open", issue.GetProperty("status").GetString()); + Assert.Equal("Missing meta description", issue.GetProperty("message").GetString()); + } + + [Fact] + public async Task Issues_status_upsert_missing_fields_returns_400() + { + var client = _factory.CreateClient(); + var response = await client.PutAsJsonAsync("/api/issues/status", new { propertyId = 1L }); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal( + "propertyId, message, and valid status required", + doc.RootElement.GetProperty("detail").GetString()); + } + + [Fact] + public async Task Issues_status_upsert_invalid_status_returns_400() + { + var client = _factory.CreateClient(); + var response = await client.PutAsJsonAsync("/api/issues/status", new + { + propertyId = 1L, + message = "x", + status = "bogus", + }); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("invalid status: bogus", doc.RootElement.GetProperty("detail").GetString()); + } + + private sealed class FakeIssueStatusRepository : IIssueStatusRepository + { + public Task> ListAsync(int propertyId, CancellationToken cancellationToken) => + Task.FromResult>( + [ + new IssueStatusRowDto + { + Id = 1, + PropertyId = propertyId, + Message = "Missing meta description", + Status = "open", + Priority = "Medium", + UpdatedAt = "2024-01-01T00:00:00+00:00", + }, + ]); + + public Task UpsertAsync( + UpsertIssueStatusRequest request, CancellationToken cancellationToken) + { + if (request.Status == "bogus") + throw new ArgumentException("invalid status: bogus"); + + return Task.FromResult(new IssueStatusRowDto + { + Id = 1, + PropertyId = request.PropertyId, + ReportId = request.ReportId, + Message = request.Message ?? string.Empty, + Status = request.Status ?? string.Empty, + Url = request.Url ?? string.Empty, + Priority = request.Priority ?? "Medium", + UpdatedAt = "2024-01-01T00:00:00+00:00", + }); + } + } + + [Fact] + public async Task Portfolio_delete_missing_ids_returns_400() + { + var client = _factory.CreateClient(); + var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Delete, "/api/portfolio/delete") + { + Content = JsonContent.Create(new { }), + }); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("reportId or crawlRunId required", doc.RootElement.GetProperty("detail").GetString()); + } + + [Fact] + public async Task Portfolio_delete_not_found_returns_404() + { + var client = _factory.CreateClient(); + var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Delete, "/api/portfolio/delete") + { + Content = JsonContent.Create(new { crawlRunId = 999L }), + }); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("portfolio item not found", doc.RootElement.GetProperty("detail").GetString()); + } + + [Fact] + public async Task Portfolio_delete_success_returns_ok() + { + var client = _factory.CreateClient(); + var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Delete, "/api/portfolio/delete") + { + Content = JsonContent.Create(new { crawlRunId = 42L }), + }); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.True(doc.RootElement.GetProperty("ok").GetBoolean()); + } + + private sealed class FakePortfolioRepository : IPortfolioRepository + { + public Task DeletePortfolioItemAsync(long? reportId, long? crawlRunId, CancellationToken cancellationToken) + { + if (reportId == 999 || crawlRunId == 999) + return Task.FromResult(false); + if (reportId is not null || crawlRunId is not null) + return Task.FromResult(true); + return Task.FromResult(false); + } + + public Task<(int ReportCount, long ReportMaxId, int CrawlCount, long CrawlMaxId)> GetCacheKeyPartsAsync( + CancellationToken cancellationToken) => + Task.FromResult((0, 0L, 0, 0L)); + + public Task> ListReportsAsync(CancellationToken cancellationToken) => + Task.FromResult>([]); + + public Task> ListReportsLatestPerDomainAsync( + CancellationToken cancellationToken) => + Task.FromResult>([]); + + public Task> ListCrawlRunsAsync(CancellationToken cancellationToken) => + Task.FromResult>([]); + + public Task> ListCrawlRunSummariesAsync( + int? maxRuns, CancellationToken cancellationToken) => + Task.FromResult>([]); + + public Task> ReadReportPayloadsPortfolioAsync( + IReadOnlyList reportIds, CancellationToken cancellationToken) => + Task.FromResult>(new Dictionary()); + + public Task ReadReportPayloadAsync(long reportId, CancellationToken cancellationToken) => + Task.FromResult(null); + + public Task FindReportIdByCrawlRunIdAsync(long crawlRunId, CancellationToken cancellationToken) => + Task.FromResult(null); + } + + private sealed class FakePortfolioService : IPortfolioService + { + public Task GetPortfolioResponseAsync( + string widget, + IReadOnlyList ids, + long? reportId, + long? crawlRunId, + CancellationToken cancellationToken) + { + if (widget.Equals("card", StringComparison.OrdinalIgnoreCase)) + { + return Task.FromResult(new PortfolioCardResponseDto + { + Group = new PortfolioGroupDto + { + DomainName = "example.com", + DomainParam = "example.com", + HealthScore = 80, + ReportId = reportId ?? 1, + }, + }); + } + + if (widget.Equals("summary", StringComparison.OrdinalIgnoreCase)) + { + return Task.FromResult(new PortfolioSummaryResponseDto + { + TotalBrands = 1, + TotalUrls = 10, + AvgHealth = 80, + }); + } + + return Task.FromResult(new PortfolioGroupsResponseDto + { + Groups = + [ + new PortfolioGroupDto + { + DomainName = "example.com", + DomainParam = "example.com", + HealthScore = 80, + ReportId = 1, + }, + ], + CrawlHistoryByDomain = new Dictionary>(), + }); + } + } + + private sealed class FakeReportRepository : IReportRepository + { + public Task GetMetaAsync(CancellationToken cancellationToken) => + Task.FromResult(new ReportMetaResponse + { + Reports = + [ + new ReportListItem + { + Id = 1, + CanonicalDomain = "example.com", + SiteName = "Example", + GeneratedAt = "2024-01-01T00:00:00", + }, + ], + CrawlRuns = [], + }); + + public Task GetPayloadDataAsync(long? reportId, string? domain, CancellationToken ct) => + Task.FromResult(reportId == 999 ? null : """{"site_name":"example.com"}"""); + + public Task ListAuditHistoryAsync(string? domain, int limit, CancellationToken ct) => + Task.FromResult(new AuditHistoryResponse + { + History = + [ + new AuditHistoryItem { ReportId = 1, CanonicalDomain = "example.com" }, + ], + }); + + public Task GetCrawlPreviewPayloadAsync(long crawlRunId, CancellationToken ct) => + Task.FromResult(new JsonObject { ["id"] = crawlRunId }); + + public Task GetMobileDeltaAsync(long runId, CancellationToken ct) => + Task.FromResult(new MobileDeltaResponse()); + } +} diff --git a/services/Data/tests/Data.Tests/Data.Tests.csproj b/services/Data/tests/Data.Tests/Data.Tests.csproj new file mode 100644 index 00000000..fd5c20c9 --- /dev/null +++ b/services/Data/tests/Data.Tests/Data.Tests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + diff --git a/services/Data/tests/Data.Tests/IssueStatusFingerprintTests.cs b/services/Data/tests/Data.Tests/IssueStatusFingerprintTests.cs new file mode 100644 index 00000000..33c12af0 --- /dev/null +++ b/services/Data/tests/Data.Tests/IssueStatusFingerprintTests.cs @@ -0,0 +1,22 @@ +using Data.Application.Issues; + +namespace Data.Tests; + +public class IssueStatusFingerprintTests +{ + [Fact] + public void Compute_matches_python_issue_fingerprint() + { + var fp = IssueStatusFingerprint.Compute("msg", "https://ex.com", "cat"); + Assert.Equal(32, fp.Length); + Assert.Equal("cf71098dd43ba87a28c89112c2dd3a43", fp); + } + + [Fact] + public void Compute_treats_null_category_as_empty() + { + var fp = IssueStatusFingerprint.Compute("msg", "https://ex.com", null); + var expected = IssueStatusFingerprint.Compute("msg", "https://ex.com", ""); + Assert.Equal(expected, fp); + } +} diff --git a/services/Data/tests/Data.Tests/NpgsqlDsnTests.cs b/services/Data/tests/Data.Tests/NpgsqlDsnTests.cs new file mode 100644 index 00000000..fea67349 --- /dev/null +++ b/services/Data/tests/Data.Tests/NpgsqlDsnTests.cs @@ -0,0 +1,81 @@ +using Data.Application.Persistence; +using Npgsql; + +namespace Data.Tests; + +public class NpgsqlDsnTests +{ + [Fact] + public void Converts_compose_dsn_to_keyword_form() + { + var result = NpgsqlDsn.ToNpgsql("postgres://profiling:profiling@postgres:5432/website_profiling"); + + var b = new NpgsqlConnectionStringBuilder(result); + Assert.Equal("postgres", b.Host); + Assert.Equal(5432, b.Port); + Assert.Equal("profiling", b.Username); + Assert.Equal("profiling", b.Password); + Assert.Equal("website_profiling", b.Database); + } + + [Fact] + public void Accepts_postgresql_scheme() + { + var b = new NpgsqlConnectionStringBuilder( + NpgsqlDsn.ToNpgsql("postgresql://u:p@db.example.com:6543/mydb")); + Assert.Equal("db.example.com", b.Host); + Assert.Equal(6543, b.Port); + Assert.Equal("mydb", b.Database); + } + + [Fact] + public void Defaults_port_when_absent() + { + var b = new NpgsqlConnectionStringBuilder( + NpgsqlDsn.ToNpgsql("postgres://u:p@localhost/website_profiling")); + Assert.Equal(5432, b.Port); + } + + [Fact] + public void Maps_connect_timeout_to_npgsql_timeout() + { + var b = new NpgsqlConnectionStringBuilder( + NpgsqlDsn.ToNpgsql("postgres://u:p@h:5432/db?connect_timeout=3")); + Assert.Equal(3, b.Timeout); + } + + [Fact] + public void Strips_unknown_query_params() + { + // Must not throw (Npgsql rejects unknown keywords); unknown params are dropped. + var b = new NpgsqlConnectionStringBuilder( + NpgsqlDsn.ToNpgsql("postgres://u:p@h:5432/db?connect_timeout=3&foo=bar&target_session_attrs=any")); + Assert.Equal("h", b.Host); + Assert.Equal(3, b.Timeout); + } + + [Fact] + public void Url_decodes_special_characters_in_password() + { + // Password "p@ss:word%1" percent-encoded in the URI userinfo. + var b = new NpgsqlConnectionStringBuilder( + NpgsqlDsn.ToNpgsql("postgres://user:p%40ss%3Aword%251@h:5432/db")); + Assert.Equal("user", b.Username); + Assert.Equal("p@ss:word%1", b.Password); + } + + [Fact] + public void Passes_through_keyword_connection_string() + { + const string keyword = "Host=h;Port=5432;Username=u;Password=p;Database=db"; + Assert.Equal(keyword, NpgsqlDsn.ToNpgsql(keyword)); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void Throws_on_empty(string raw) + { + Assert.Throws(() => NpgsqlDsn.ToNpgsql(raw)); + } +} diff --git a/services/Data/tests/Data.Tests/PortfolioGroupingTests.cs b/services/Data/tests/Data.Tests/PortfolioGroupingTests.cs new file mode 100644 index 00000000..76bb808b --- /dev/null +++ b/services/Data/tests/Data.Tests/PortfolioGroupingTests.cs @@ -0,0 +1,66 @@ +using System.Text.Json; +using Data.Application.Dto.Portfolio; +using Data.Application.Portfolio; + +namespace Data.Tests; + +public class PortfolioGroupingTests +{ + [Fact] + public void Compute_domain_groups_builds_expected_shape() + { + const string payloadJson = """ + { + "site_name": "Example", + "summary": { "count_2xx": 8, "count_3xx": 0, "count_4xx": 1, "count_5xx": 0, "count_error": 0, "total_urls": 9 }, + "categories": [ + { "id": "technical_seo", "name": "Technical SEO", "score": 80, "issues": [{ "priority": "High" }] } + ], + "top_pages": [{ "url": "https://example.com/" }], + "crawl_run_id": 5 + } + """; + + using var doc = JsonDocument.Parse(payloadJson); + var payload = doc.RootElement.Clone(); + + var maps = new PortfolioMaps + { + StartUrlByRunId = new Dictionary { [5] = "https://example.com/" }, + RunCreatedAtByRunId = new Dictionary { [5] = "2024-01-01T00:00:00+00:00" }, + RunMetaByRunId = new Dictionary(), + CrawlSummaries = [], + }; + + var reports = new List + { + new() { Id = 1, GeneratedAt = "2024-01-02T00:00:00+00:00" }, + }; + + var groups = PortfolioGrouping.ComputeDomainGroups(reports, maps, _ => payload); + + Assert.Single(groups); + var group = groups[0]; + Assert.Equal("example.com", group.DomainName); + Assert.Equal(80, group.HealthScore); + Assert.Equal(1, group.ReportId); + Assert.Equal(5, group.CrawlRunId); + Assert.Equal(1, group.IssueCounts.High); + Assert.Equal("example.com", group.DomainParam); + } + + [Fact] + public void Compute_summary_averages_health_scores() + { + var groups = new List + { + new() { HealthScore = 80, UrlCount = 10 }, + new() { HealthScore = 60, UrlCount = 5 }, + }; + + var summary = PortfolioGrouping.ComputeSummary(groups); + Assert.Equal(2, summary.TotalBrands); + Assert.Equal(15, summary.TotalUrls); + Assert.Equal(70, summary.AvgHealth); + } +} diff --git a/services/Data/tests/Data.Tests/PyIsoTests.cs b/services/Data/tests/Data.Tests/PyIsoTests.cs new file mode 100644 index 00000000..3e397190 --- /dev/null +++ b/services/Data/tests/Data.Tests/PyIsoTests.cs @@ -0,0 +1,36 @@ +using Data.Application.Json; + +namespace Data.Tests; + +public class PyIsoTests +{ + [Fact] + public void Omits_fraction_when_microseconds_zero() + { + var value = new DateTimeOffset(2026, 6, 24, 15, 30, 0, TimeSpan.Zero); + Assert.Equal("2026-06-24T15:30:00+00:00", PyIso.Format(value)); + } + + [Fact] + public void Renders_six_digit_microseconds_with_trailing_zeros() + { + // 123000 microseconds → Python isoformat keeps all 6 digits ("...123000"), not ".123". + var value = new DateTimeOffset(2026, 6, 24, 15, 30, 0, TimeSpan.Zero).AddTicks(123000 * 10); + Assert.Equal("2026-06-24T15:30:00.123000+00:00", PyIso.Format(value)); + } + + [Fact] + public void Renders_full_microsecond_precision() + { + var value = new DateTimeOffset(2026, 6, 24, 15, 30, 0, TimeSpan.Zero).AddTicks(123456 * 10); + Assert.Equal("2026-06-24T15:30:00.123456+00:00", PyIso.Format(value)); + } + + [Fact] + public void Normalizes_non_utc_offset_to_utc() + { + // 21:00+05:30 is the same instant as 15:30Z; Python/psycopg surface UTC, so we render +00:00. + var value = new DateTimeOffset(2026, 6, 24, 21, 0, 0, TimeSpan.FromHours(5.5)); + Assert.Equal("2026-06-24T15:30:00+00:00", PyIso.Format(value)); + } +} diff --git a/services/FileService/src/FileService.Api/Program.cs b/services/FileService/src/FileService.Api/Program.cs index b6de222c..99d0c241 100644 --- a/services/FileService/src/FileService.Api/Program.cs +++ b/services/FileService/src/FileService.Api/Program.cs @@ -1,3 +1,4 @@ +using System.Text; using FileService.Api; using FileService.Application; using FileService.Application.Services; @@ -153,8 +154,57 @@ "Export crawl workbook by domain", "Resolves the latest report for the domain, then generates an Excel crawl workbook."); +// ── CSV / JSON / sitemap exports (migrated from the Python report API) ────────── +// All three render from the report payload read directly from Postgres. + +app.MapGet("/v1/reports/{reportId:int}/csv", (int reportId, IReportExportService export, string? disposition, CancellationToken ct) => + ExportText(() => export.GetCsvByReportIdAsync(reportId, ct), "text/csv", disposition, $"report-{reportId}.csv")) + .WithName("GetReportCsvById").WithTags("Export"); + +app.MapGet("/v1/reports/by-domain/{domain}/csv", (string domain, IReportExportService export, string? disposition, CancellationToken ct) => + ExportText(() => export.GetCsvByDomainAsync(domain, ct), "text/csv", disposition, $"report-{SafeName(domain)}.csv")) + .WithName("GetReportCsvByDomain").WithTags("Export"); + +app.MapGet("/v1/reports/{reportId:int}/json", (int reportId, IReportExportService export, string? disposition, CancellationToken ct) => + ExportText(() => export.GetJsonByReportIdAsync(reportId, ct), "application/json", disposition, $"report-{reportId}.json")) + .WithName("GetReportJsonById").WithTags("Export"); + +app.MapGet("/v1/reports/by-domain/{domain}/json", (string domain, IReportExportService export, string? disposition, CancellationToken ct) => + ExportText(() => export.GetJsonByDomainAsync(domain, ct), "application/json", disposition, $"report-{SafeName(domain)}.json")) + .WithName("GetReportJsonByDomain").WithTags("Export"); + +app.MapGet("/v1/reports/{reportId:int}/sitemap", (int reportId, IReportExportService export, string? disposition, CancellationToken ct) => + ExportText(() => export.GetSitemapByReportIdAsync(reportId, ct), "application/xml", disposition, $"sitemap-{reportId}.xml")) + .WithName("GetReportSitemapById").WithTags("Export"); + +app.MapGet("/v1/reports/by-domain/{domain}/sitemap", (string domain, IReportExportService export, string? disposition, CancellationToken ct) => + ExportText(() => export.GetSitemapByDomainAsync(domain, ct), "application/xml", disposition, $"sitemap-{SafeName(domain)}.xml")) + .WithName("GetReportSitemapByDomain").WithTags("Export"); + app.Run(); +static async Task ExportText(Func> render, string contentType, string? disposition, string filename) +{ + try + { + var text = await render(); + var inline = string.Equals(disposition, "inline", StringComparison.OrdinalIgnoreCase); + var contentDisposition = inline ? "inline" : $"attachment; filename=\"{filename}\""; + return new BinaryFileResult(Encoding.UTF8.GetBytes(text), contentType, contentDisposition); + } + catch (KeyNotFoundException ex) + { + return Results.NotFound(new { detail = ex.Message }); + } + catch (HttpRequestException ex) + { + return Results.Json(new { detail = "Upstream data service unavailable", error = ex.Message }, statusCode: 502); + } +} + +static string SafeName(string? domain) => + string.IsNullOrWhiteSpace(domain) ? "report" : domain.Replace('.', '-'); + static PdfProfile ParseProfile(string? profile) => (profile ?? "standard").Trim().ToLowerInvariant() switch { "executive" => PdfProfile.Executive, diff --git a/services/FileService/src/FileService.Application/Clients/DbReportDataClient.cs b/services/FileService/src/FileService.Application/Clients/DbReportDataClient.cs new file mode 100644 index 00000000..27351872 --- /dev/null +++ b/services/FileService/src/FileService.Application/Clients/DbReportDataClient.cs @@ -0,0 +1,46 @@ +using System.Text.Json; +using FileService.Application.Persistence; +using FileService.Domain.Models; +using Microsoft.EntityFrameworkCore; + +namespace FileService.Application.Clients; + +/// +/// Reads report payloads directly from Postgres (read-only EF Core), replacing the old HTTP hop to the +/// Python report API. Implements the same contract, so PdfReportService, +/// WorkbookReportService and the export services are unaffected by the data-source change. +/// +public sealed class DbReportDataClient(ReportDbContext db) : IReportDataClient +{ + public async Task> ListReportsAsync(CancellationToken cancellationToken = default) + { + var rows = await db.ReportPayloads + .OrderByDescending(r => r.Id) + .Select(r => new { r.Id, r.CanonicalDomain, r.SiteName, r.GeneratedAt }) + .ToListAsync(cancellationToken); + + return rows.Select(r => new ReportListRow + { + Id = (int)r.Id, + CanonicalDomain = r.CanonicalDomain, + SiteName = r.SiteName, + GeneratedAt = r.GeneratedAt.ToString("o"), + }).ToList(); + } + + public async Task GetPayloadAsync(int reportId, CancellationToken cancellationToken = default) + { + var data = await db.ReportPayloads + .Where(r => r.Id == reportId) + .Select(r => r.Data) + .FirstOrDefaultAsync(cancellationToken); + + if (data is null) + { + return null; + } + + using var doc = JsonDocument.Parse(data); + return doc.RootElement.Clone(); + } +} diff --git a/services/FileService/src/FileService.Application/Clients/ReportDataClient.cs b/services/FileService/src/FileService.Application/Clients/ReportDataClient.cs deleted file mode 100644 index 5780b9f3..00000000 --- a/services/FileService/src/FileService.Application/Clients/ReportDataClient.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Net; -using System.Text.Json; -using FileService.Application.Options; -using FileService.Domain.Models; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace FileService.Application.Clients; - -public sealed class ReportDataClient : IReportDataClient -{ - private static readonly JsonSerializerOptions JsonOptions = new() - { - PropertyNameCaseInsensitive = true, - }; - - private readonly HttpClient _http; - private readonly ILogger _logger; - - public ReportDataClient(HttpClient http, IOptions options, ILogger logger) - { - _http = http; - _logger = logger; - var baseUrl = options.Value.BaseUrl.TrimEnd('/'); - _http.BaseAddress = new Uri(baseUrl + "/"); - _http.Timeout = TimeSpan.FromSeconds(Math.Max(5, options.Value.TimeoutSeconds)); - } - - public async Task> ListReportsAsync(CancellationToken cancellationToken = default) - { - using var response = await _http.GetAsync("api/report/meta", cancellationToken); - await EnsureSuccessAsync(response, cancellationToken); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); - using var doc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); - if (!doc.RootElement.TryGetProperty("reports", out var reportsEl) || reportsEl.ValueKind != JsonValueKind.Array) - { - return []; - } - - var rows = new List(); - foreach (var item in reportsEl.EnumerateArray()) - { - rows.Add(new ReportListRow - { - Id = item.TryGetProperty("id", out var idEl) ? idEl.GetInt32() : 0, - CanonicalDomain = GetString(item, "canonical_domain"), - SiteName = GetString(item, "site_name"), - GeneratedAt = GetString(item, "generated_at"), - }); - } - return rows; - } - - public async Task GetPayloadAsync(int reportId, CancellationToken cancellationToken = default) - { - using var response = await _http.GetAsync($"api/report/payload?reportId={reportId}", cancellationToken); - if (response.StatusCode == HttpStatusCode.NotFound) - { - return null; - } - await EnsureSuccessAsync(response, cancellationToken); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); - using var doc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); - if (!doc.RootElement.TryGetProperty("payload", out var payload)) - { - return null; - } - return payload.Clone(); - } - - private static string? GetString(JsonElement el, string name) - { - if (!el.TryGetProperty(name, out var prop) || prop.ValueKind == JsonValueKind.Null) - { - return null; - } - return prop.GetString(); - } - - private async Task EnsureSuccessAsync(HttpResponseMessage response, CancellationToken cancellationToken) - { - if (response.IsSuccessStatusCode) - { - return; - } - var body = await response.Content.ReadAsStringAsync(cancellationToken); - _logger.LogWarning("Report API request failed: {Status} {Body}", response.StatusCode, body); - throw new HttpRequestException($"Report API returned {(int)response.StatusCode}: {body}", null, response.StatusCode); - } -} diff --git a/services/FileService/src/FileService.Application/DependencyInjection.cs b/services/FileService/src/FileService.Application/DependencyInjection.cs index ed35ea61..f2ef9842 100644 --- a/services/FileService/src/FileService.Application/DependencyInjection.cs +++ b/services/FileService/src/FileService.Application/DependencyInjection.cs @@ -1,8 +1,13 @@ using FileService.Application.Clients; +using FileService.Application.Persistence; using FileService.Application.Options; using FileService.Application.Services; using FileService.Rendering; +using FileService.Rendering.Exports; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Npgsql; namespace FileService.Application; @@ -10,6 +15,8 @@ public static class DependencyInjection { public static IServiceCollection AddFileServiceApplication(this IServiceCollection services) { + // App-settings / branding still come over HTTP (AppSettingsClient); only report payloads + // move to direct Postgres access below. services.AddOptions() .BindConfiguration(ReportApiOptions.SectionName) .PostConfigure(o => @@ -21,13 +28,50 @@ public static IServiceCollection AddFileServiceApplication(this IServiceCollecti } }); - services.AddHttpClient(); + // Read-only Postgres access for report payloads — replaces the old HTTP hop to the Python + // report API. Connection string comes from DATABASE_URL (libpq URI), same as the Data service. + // The data source + context are built lazily, so callers that inject a fake IReportDataClient + // (e.g. integration tests) never touch the DB. + services.AddOptions() + .BindConfiguration(DatabaseOptions.SectionName) + .PostConfigure(o => + { + var url = Environment.GetEnvironmentVariable("DATABASE_URL"); + if (!string.IsNullOrWhiteSpace(url)) + { + o.ConnectionString = url.Trim(); + } + }); + + services.AddSingleton(sp => + { + var o = sp.GetRequiredService>().Value; + var builder = new NpgsqlDataSourceBuilder(NpgsqlDsn.ToNpgsql(o.ConnectionString)); + builder.ConnectionStringBuilder.MinPoolSize = o.MinPoolSize; + builder.ConnectionStringBuilder.MaxPoolSize = o.MaxPoolSize; + return builder.Build(); + }); + + services.AddDbContextPool((sp, options) => + { + var o = sp.GetRequiredService>().Value; + var dataSource = sp.GetRequiredService(); + options + .UseNpgsql(dataSource, npg => npg.CommandTimeout(o.CommandTimeoutSeconds)) + .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); + }); + + services.AddScoped(); services.AddHttpClient(); services.AddHttpClient(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } } diff --git a/services/FileService/src/FileService.Application/FileService.Application.csproj b/services/FileService/src/FileService.Application/FileService.Application.csproj index 9a212f4c..901eb4ae 100644 --- a/services/FileService/src/FileService.Application/FileService.Application.csproj +++ b/services/FileService/src/FileService.Application/FileService.Application.csproj @@ -8,6 +8,7 @@ + diff --git a/services/FileService/src/FileService.Application/Mapping/ChapterMappers.cs b/services/FileService/src/FileService.Application/Mapping/ChapterMappers.cs index ea7829b1..a1ebb120 100644 --- a/services/FileService/src/FileService.Application/Mapping/ChapterMappers.cs +++ b/services/FileService/src/FileService.Application/Mapping/ChapterMappers.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Text.Json; using FileService.Domain.Models; @@ -231,7 +232,11 @@ internal static class JsonHelper return prop.ValueKind switch { JsonValueKind.String => prop.GetString(), - JsonValueKind.Number => prop.GetRawText(), + // Normalise numbers to a plain decimal string; GetRawText() would + // leak JSON formatting like scientific notation (e.g. "1E+10"). + JsonValueKind.Number => prop.TryGetInt64(out var l) + ? l.ToString(CultureInfo.InvariantCulture) + : prop.GetDouble().ToString(CultureInfo.InvariantCulture), _ => null, }; } diff --git a/services/FileService/src/FileService.Application/Options/DatabaseOptions.cs b/services/FileService/src/FileService.Application/Options/DatabaseOptions.cs new file mode 100644 index 00000000..35e2cf0b --- /dev/null +++ b/services/FileService/src/FileService.Application/Options/DatabaseOptions.cs @@ -0,0 +1,23 @@ +namespace FileService.Application.Options; + +/// +/// Postgres connection settings for FileService's read-only report access. +/// is the libpq URI from env DATABASE_URL (the same value the Python / Data services use); it is +/// converted to an Npgsql keyword connection string by NpgsqlDsn.ToNpgsql. +/// +public sealed class DatabaseOptions +{ + public const string SectionName = "Database"; + + /// libpq URI or Npgsql keyword string (env override: DATABASE_URL). + public string ConnectionString { get; set; } = ""; + + /// Minimum pooled connections. + public int MinPoolSize { get; set; } = 1; + + /// Maximum pooled connections. + public int MaxPoolSize { get; set; } = 10; + + /// Per-query command timeout (seconds). + public int CommandTimeoutSeconds { get; set; } = 30; +} diff --git a/services/FileService/src/FileService.Application/Options/ReportApiOptions.cs b/services/FileService/src/FileService.Application/Options/ReportApiOptions.cs index c32dced4..9102692b 100644 --- a/services/FileService/src/FileService.Application/Options/ReportApiOptions.cs +++ b/services/FileService/src/FileService.Application/Options/ReportApiOptions.cs @@ -1,8 +1,9 @@ namespace FileService.Application.Options; /// -/// HTTP base URL for the Site Audit report API (JSON payload, meta, app-settings). -/// FileService is not tied to any specific backend framework — only this HTTP contract. +/// HTTP base URL for the Site Audit app-settings / branding API (used by AppSettingsClient). +/// Report payloads are no longer fetched over HTTP — they are read directly from Postgres +/// (see DbReportDataClient / DatabaseOptions). /// public sealed class ReportApiOptions { diff --git a/services/FileService/src/FileService.Application/Persistence/NpgsqlDsn.cs b/services/FileService/src/FileService.Application/Persistence/NpgsqlDsn.cs new file mode 100644 index 00000000..5f92e267 --- /dev/null +++ b/services/FileService/src/FileService.Application/Persistence/NpgsqlDsn.cs @@ -0,0 +1,92 @@ +using Npgsql; + +namespace FileService.Application.Persistence; + +/// +/// Converts a libpq connection URI (postgres://user:pass@host:port/db?connect_timeout=3) — the +/// form used by DATABASE_URL across the Python services — into an Npgsql keyword connection +/// string. Npgsql's parser does NOT accept the postgres:// URI form and throws on it, so this +/// conversion is mandatory. A string that is already in keyword form is passed through unchanged. +/// (Copied from the Data service so FileService owns its own DB access.) +/// +public static class NpgsqlDsn +{ + public static string ToNpgsql(string raw) + { + if (string.IsNullOrWhiteSpace(raw)) + { + throw new InvalidOperationException( + "DATABASE_URL is not set. Example: postgres://user:pass@host:5432/website_profiling"); + } + + var s = raw.Trim(); + var isUri = s.StartsWith("postgres://", StringComparison.OrdinalIgnoreCase) + || s.StartsWith("postgresql://", StringComparison.OrdinalIgnoreCase); + if (!isUri) + { + return s; // already an Npgsql keyword connection string + } + + var uri = new Uri(s); + var b = new NpgsqlConnectionStringBuilder + { + Host = Uri.UnescapeDataString(uri.Host), + Port = uri.IsDefaultPort || uri.Port <= 0 ? 5432 : uri.Port, + Database = Uri.UnescapeDataString(uri.AbsolutePath.TrimStart('/')), + }; + + var userInfo = uri.UserInfo.Split(':', 2); + if (userInfo.Length > 0 && userInfo[0].Length > 0) + { + b.Username = Uri.UnescapeDataString(userInfo[0]); + } + if (userInfo.Length > 1) + { + b.Password = Uri.UnescapeDataString(userInfo[1]); + } + + // Translate the query params we understand; strip the rest (Npgsql throws on unknown keywords). + foreach (var (key, value) in ParseQuery(uri.Query)) + { + switch (key.ToLowerInvariant()) + { + case "connect_timeout": + if (int.TryParse(value, out var t)) + { + b.Timeout = t; // Npgsql "Timeout" == connect timeout (seconds) + } + break; + case "sslmode": + if (Enum.TryParse(value, ignoreCase: true, out var mode)) + { + b.SslMode = mode; + } + break; + case "application_name": + b.ApplicationName = value; + break; + // unknown params are intentionally dropped + } + } + + return b.ConnectionString; + } + + private static IEnumerable> ParseQuery(string query) + { + var q = query.TrimStart('?'); + if (q.Length == 0) + { + yield break; + } + foreach (var part in q.Split('&', StringSplitOptions.RemoveEmptyEntries)) + { + var idx = part.IndexOf('='); + yield return idx < 0 + ? new KeyValuePair(Uri.UnescapeDataString(part), "") + : new KeyValuePair( + Uri.UnescapeDataString(part[..idx]), + Uri.UnescapeDataString(part[(idx + 1)..])); + } + } +} diff --git a/services/FileService/src/FileService.Application/Persistence/ReportDbContext.cs b/services/FileService/src/FileService.Application/Persistence/ReportDbContext.cs new file mode 100644 index 00000000..22a5d5df --- /dev/null +++ b/services/FileService/src/FileService.Application/Persistence/ReportDbContext.cs @@ -0,0 +1,29 @@ +using FileService.Domain.Models; +using Microsoft.EntityFrameworkCore; + +namespace FileService.Application.Persistence; + +/// +/// Read-only EF Core context over the Alembic-owned report_payload table. It NEVER creates or +/// migrates tables (no Design reference, no Migrations folder, Migrate()/EnsureCreated() never called) +/// and tracking is disabled globally. Mirrors the Data service's DataDbContext, scoped to just the +/// one table FileService needs to render exports. +/// +public sealed class ReportDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet ReportPayloads => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(e => + { + e.ToTable("report_payload"); + e.HasKey(x => x.Id); + e.Property(x => x.Id).HasColumnName("id"); + e.Property(x => x.GeneratedAt).HasColumnName("generated_at"); + e.Property(x => x.SiteName).HasColumnName("site_name"); + e.Property(x => x.CanonicalDomain).HasColumnName("canonical_domain"); + e.Property(x => x.Data).HasColumnName("data").HasColumnType("jsonb"); + }); + } +} diff --git a/services/FileService/src/FileService.Application/Services/ReportExportService.cs b/services/FileService/src/FileService.Application/Services/ReportExportService.cs new file mode 100644 index 00000000..b93cc10f --- /dev/null +++ b/services/FileService/src/FileService.Application/Services/ReportExportService.cs @@ -0,0 +1,67 @@ +using System.Text.Json; +using FileService.Application.Clients; +using FileService.Application.Domain; +using FileService.Rendering.Exports; + +namespace FileService.Application.Services; + +public interface IReportExportService +{ + Task GetCsvByReportIdAsync(int reportId, CancellationToken cancellationToken = default); + Task GetCsvByDomainAsync(string domain, CancellationToken cancellationToken = default); + Task GetJsonByReportIdAsync(int reportId, CancellationToken cancellationToken = default); + Task GetJsonByDomainAsync(string domain, CancellationToken cancellationToken = default); + Task GetSitemapByReportIdAsync(int reportId, CancellationToken cancellationToken = default); + Task GetSitemapByDomainAsync(string domain, CancellationToken cancellationToken = default); +} + +/// +/// Renders CSV / JSON / sitemap exports from the report payload. Mirrors : +/// fetch the payload via (now Postgres-backed), then hand it to the +/// matching exporter. Throws when the report/domain has no payload. +/// +public sealed class ReportExportService( + IReportDataClient client, + ReportCsvExporter csv, + ReportJsonExporter json, + ReportSitemapExporter sitemap) : IReportExportService +{ + public Task GetCsvByReportIdAsync(int reportId, CancellationToken cancellationToken = default) => + RenderByIdAsync(reportId, csv.Generate, cancellationToken); + + public Task GetCsvByDomainAsync(string domain, CancellationToken cancellationToken = default) => + RenderByDomainAsync(domain, csv.Generate, cancellationToken); + + public Task GetJsonByReportIdAsync(int reportId, CancellationToken cancellationToken = default) => + RenderByIdAsync(reportId, json.Generate, cancellationToken); + + public Task GetJsonByDomainAsync(string domain, CancellationToken cancellationToken = default) => + RenderByDomainAsync(domain, json.Generate, cancellationToken); + + public Task GetSitemapByReportIdAsync(int reportId, CancellationToken cancellationToken = default) => + RenderByIdAsync(reportId, p => sitemap.Generate(p), cancellationToken); + + public Task GetSitemapByDomainAsync(string domain, CancellationToken cancellationToken = default) => + RenderByDomainAsync(domain, p => sitemap.Generate(p), cancellationToken); + + private async Task RenderByIdAsync(int reportId, Func render, CancellationToken ct) + { + var payload = await client.GetPayloadAsync(reportId, ct); + if (payload is null) + { + throw new KeyNotFoundException($"Report {reportId} not found"); + } + return render(payload.Value); + } + + private async Task RenderByDomainAsync(string domain, Func render, CancellationToken ct) + { + var reports = await client.ListReportsAsync(ct); + var reportId = DomainResolver.ResolveReportId(reports, domain); + if (reportId is null) + { + throw new KeyNotFoundException($"No report found for domain '{domain}'"); + } + return await RenderByIdAsync(reportId.Value, render, ct); + } +} diff --git a/services/FileService/src/FileService.Domain/Models/ReportPayloadRow.cs b/services/FileService/src/FileService.Domain/Models/ReportPayloadRow.cs new file mode 100644 index 00000000..c4f6a1a5 --- /dev/null +++ b/services/FileService/src/FileService.Domain/Models/ReportPayloadRow.cs @@ -0,0 +1,21 @@ +namespace FileService.Domain.Models; + +/// +/// Read-only mapping of the existing report_payload table (owned by Alembic migrations). +/// FileService never writes or migrates this table; it only reads the JSON payload to render +/// PDF / Excel / CSV / JSON / sitemap exports. Mirrors the Data service's ReportPayload entity. +/// +public sealed class ReportPayloadRow +{ + public long Id { get; set; } + + /// generated_at TIMESTAMPTZ. + public DateTimeOffset GeneratedAt { get; set; } + + public string? SiteName { get; set; } + + public string? CanonicalDomain { get; set; } + + /// data JSONB as raw JSON text; parsed with System.Text.Json by callers. + public string Data { get; set; } = "{}"; +} diff --git a/services/FileService/src/FileService.Rendering/AuditWorkbookGenerator.cs b/services/FileService/src/FileService.Rendering/AuditWorkbookGenerator.cs index 8fab46f5..cefe669e 100644 --- a/services/FileService/src/FileService.Rendering/AuditWorkbookGenerator.cs +++ b/services/FileService/src/FileService.Rendering/AuditWorkbookGenerator.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Text.Json; using ClosedXML.Excel; @@ -225,6 +226,20 @@ JsonValueKind.Number when el.TryGetInt64(out var l) => l, _ => el.ToString(), }; + private static string CustomFieldString(JsonElement el) => el.ValueKind switch + { + JsonValueKind.Null or JsonValueKind.Undefined => "", + JsonValueKind.String => el.GetString() ?? "", + JsonValueKind.True => "true", + JsonValueKind.False => "false", + // Plain decimal string instead of raw JSON (avoids scientific notation). + JsonValueKind.Number => el.TryGetInt64(out var l) + ? l.ToString(CultureInfo.InvariantCulture) + : el.GetDouble().ToString(CultureInfo.InvariantCulture), + // Nested object/array can't fit a flat cell as a scalar — compact JSON. + _ => el.GetRawText(), + }; + private static Dictionary ParseCustomFields(JsonElement link) { if (!link.TryGetProperty("custom_fields", out var raw)) @@ -237,7 +252,7 @@ private static Dictionary ParseCustomFields(JsonElement link) var dict = new Dictionary(StringComparer.Ordinal); foreach (var prop in raw.EnumerateObject()) { - dict[prop.Name] = prop.Value.ToString(); + dict[prop.Name] = CustomFieldString(prop.Value); } return dict; @@ -265,7 +280,7 @@ private static Dictionary ParseCustomFields(JsonElement link) var parsed = new Dictionary(StringComparer.Ordinal); foreach (var prop in doc.RootElement.EnumerateObject()) { - parsed[prop.Name] = prop.Value.ToString(); + parsed[prop.Name] = CustomFieldString(prop.Value); } return parsed; diff --git a/services/FileService/src/FileService.Rendering/Exports/ReportCsvExporter.cs b/services/FileService/src/FileService.Rendering/Exports/ReportCsvExporter.cs new file mode 100644 index 00000000..bd4c6024 --- /dev/null +++ b/services/FileService/src/FileService.Rendering/Exports/ReportCsvExporter.cs @@ -0,0 +1,204 @@ +using System.Text; +using System.Text.Json; + +namespace FileService.Rendering.Exports; + +/// +/// CSV export of a report payload. Faithful port of the Python export_audit_csv + +/// export_audit_data helpers (_executive_export_data, _executive_source_label, +/// _issues_rows, _issue_recommendation, category_display_name). Section/column +/// order and CSV dialect (comma, double-quote, CRLF line endings, minimal quoting) match Python's +/// csv.writer so existing CSV consumers are unaffected. +/// +public sealed class ReportCsvExporter +{ + // LEGACY_CATEGORY_DISPLAY from reporting/terminology.py — remaps older category names only. + private static readonly Dictionary LegacyCategoryDisplay = new(StringComparer.Ordinal) + { + ["HTML & Accessibility"] = "Accessibility & markup", + ["HTML/Accessibility"] = "Accessibility & markup", + ["Link Health"] = "Links", + ["Mobile Optimization"] = "Mobile SEO", + ["Content intelligence"] = "Content quality", + }; + + private static readonly char[] CsvSpecial = [',', '"', '\r', '\n']; + + public string Generate(JsonElement payload) + { + var sb = new StringBuilder(); + var obj = payload.ValueKind == JsonValueKind.Object ? payload : default; + + Row(sb, "# Site Audit export"); + Row(sb, "site_name", Cell(obj, "site_name")); + Row(sb, "report_generated_at", Cell(obj, "report_generated_at")); + + if (obj.ValueKind == JsonValueKind.Object + && obj.TryGetProperty("report_meta", out var meta) + && meta.ValueKind == JsonValueKind.Object + && meta.EnumerateObject().Any()) + { + var sources = new List(); + if (meta.TryGetProperty("data_sources", out var ds) && ds.ValueKind == JsonValueKind.Array) + { + foreach (var d in ds.EnumerateArray()) + { + sources.Add(d.ValueKind == JsonValueKind.String ? d.GetString() ?? "" : d.GetRawText()); + } + } + Row(sb, "data_sources", string.Join(", ", sources)); + } + + Row(sb); // blank + Row(sb, "url", "status", "title", "inlinks", "word_count"); + foreach (var link in ArrayItems(obj, "links")) + { + if (link.ValueKind != JsonValueKind.Object) + { + continue; + } + Row(sb, + Cell(link, "url"), Cell(link, "status"), Cell(link, "title"), + Cell(link, "inlinks"), Cell(link, "word_count")); + } + + var (summary, priorities, source) = Executive(obj); + if (summary.Length > 0 || priorities.Count > 0) + { + Row(sb); // blank + Row(sb, "# Executive summary"); + Row(sb, "source", ExecutiveSourceLabel(source)); + if (summary.Length > 0) + { + Row(sb, "summary", summary); + } + for (var i = 0; i < priorities.Count; i++) + { + Row(sb, $"priority_{i + 1}", priorities[i]); + } + } + + Row(sb); // blank + Row(sb, "category", "priority", "message", "url", "recommendation", "llm_recommendation"); + foreach (var cat in ArrayItems(obj, "categories")) + { + if (cat.ValueKind != JsonValueKind.Object) + { + continue; + } + var ui = CategoryDisplayName(Cell(cat, "name")); + foreach (var issue in ArrayItems(cat, "issues")) + { + if (issue.ValueKind != JsonValueKind.Object) + { + continue; + } + var (rec, llm) = IssueRecommendation(issue); + Row(sb, + ui, Cell(issue, "priority"), Cell(issue, "message"), + Cell(issue, "url"), rec, llm); + } + } + + return sb.ToString(); + } + + private static string CategoryDisplayName(string name) => + string.IsNullOrEmpty(name) + ? "" + : LegacyCategoryDisplay.TryGetValue(name, out var v) ? v : name; + + private static (string Display, string Llm) IssueRecommendation(JsonElement issue) + { + var rule = Cell(issue, "recommendation").Trim(); + var llm = Cell(issue, "llm_recommendation").Trim(); + if (llm.Length > 0 && llm != rule) + { + return (llm, llm); + } + return (llm.Length > 0 ? llm : rule, llm); + } + + private static (string Summary, List Priorities, string Source) Executive(JsonElement payload) + { + var summary = ""; + var source = ""; + var priorities = new List(); + if (payload.ValueKind == JsonValueKind.Object + && payload.TryGetProperty("executive_summary", out var es) + && es.ValueKind == JsonValueKind.Object) + { + summary = Cell(es, "summary").Trim(); + source = Cell(es, "source").Trim(); + if (es.TryGetProperty("priorities", out var pr) && pr.ValueKind == JsonValueKind.Array) + { + foreach (var p in pr.EnumerateArray()) + { + var s = (p.ValueKind == JsonValueKind.String ? p.GetString() ?? "" : p.GetRawText()).Trim(); + if (s.Length > 0) + { + priorities.Add(s); + } + } + } + } + return (summary, priorities, source); + } + + private static string ExecutiveSourceLabel(string source) => source switch + { + "ai_insights" => "AI insights", + "deterministic" => "Measured + Search Console", + _ => string.IsNullOrEmpty(source) ? "Audit data" : source, + }; + + private static IEnumerable ArrayItems(JsonElement obj, string name) + { + if (obj.ValueKind == JsonValueKind.Object + && obj.TryGetProperty(name, out var arr) + && arr.ValueKind == JsonValueKind.Array) + { + foreach (var item in arr.EnumerateArray()) + { + yield return item; + } + } + } + + private static string Cell(JsonElement obj, string name) + { + if (obj.ValueKind != JsonValueKind.Object || !obj.TryGetProperty(name, out var v)) + { + return ""; + } + return v.ValueKind switch + { + JsonValueKind.String => v.GetString() ?? "", + JsonValueKind.Number => v.GetRawText(), + JsonValueKind.True => "True", + JsonValueKind.False => "False", + _ => "", + }; + } + + private static void Row(StringBuilder sb, params string[] cells) + { + for (var i = 0; i < cells.Length; i++) + { + if (i > 0) + { + sb.Append(','); + } + sb.Append(CsvEscape(cells[i])); + } + sb.Append("\r\n"); + } + + private static string CsvEscape(string? s) + { + s ??= ""; + return s.IndexOfAny(CsvSpecial) >= 0 + ? "\"" + s.Replace("\"", "\"\"") + "\"" + : s; + } +} diff --git a/services/FileService/src/FileService.Rendering/Exports/ReportJsonExporter.cs b/services/FileService/src/FileService.Rendering/Exports/ReportJsonExporter.cs new file mode 100644 index 00000000..5edc02c1 --- /dev/null +++ b/services/FileService/src/FileService.Rendering/Exports/ReportJsonExporter.cs @@ -0,0 +1,14 @@ +using System.Text.Json; + +namespace FileService.Rendering.Exports; + +/// +/// JSON export = the report payload, pretty-printed. Mirrors the Python +/// export_audit_json (json.dumps(payload, indent=2)). +/// +public sealed class ReportJsonExporter +{ + private static readonly JsonSerializerOptions Indented = new() { WriteIndented = true }; + + public string Generate(JsonElement payload) => JsonSerializer.Serialize(payload, Indented); +} diff --git a/services/FileService/src/FileService.Rendering/Exports/ReportSitemapExporter.cs b/services/FileService/src/FileService.Rendering/Exports/ReportSitemapExporter.cs new file mode 100644 index 00000000..34cd4202 --- /dev/null +++ b/services/FileService/src/FileService.Rendering/Exports/ReportSitemapExporter.cs @@ -0,0 +1,94 @@ +using System.Text; +using System.Text.Json; + +namespace FileService.Rendering.Exports; + +/// +/// XML sitemap of indexable, 2xx URLs from the report payload's links. Faithful port of the +/// Python build_sitemap_xml (skips noindex + non-2xx, XML-escapes loc, caps at max_urls). +/// +public sealed class ReportSitemapExporter +{ + public string Generate(JsonElement payload, int maxUrls = 50000) + { + var urls = new List(); + if (payload.ValueKind == JsonValueKind.Object + && payload.TryGetProperty("links", out var links) + && links.ValueKind == JsonValueKind.Array) + { + foreach (var row in links.EnumerateArray()) + { + if (row.ValueKind != JsonValueKind.Object) + { + continue; + } + if (IsTruthy(row, "noindex")) + { + continue; + } + var status = ScalarString(row, "status"); + if (!status.StartsWith("2", StringComparison.Ordinal)) + { + continue; + } + var url = ScalarString(row, "url").Trim(); + if (url.Length > 0) + { + urls.Add(url); + } + } + } + + var cap = Math.Max(1, maxUrls); + if (urls.Count > cap) + { + urls = urls.GetRange(0, cap); + } + + var body = string.Join( + "\n", + urls.Select(u => $" {XmlEscape(u)}")); + + var sb = new StringBuilder(); + sb.Append("\n"); + sb.Append("\n"); + sb.Append(body).Append('\n'); + sb.Append("\n"); + return sb.ToString(); + } + + // Mirrors xml.sax.saxutils.escape (default escapes only & < >, in that order). + private static string XmlEscape(string s) => + s.Replace("&", "&").Replace("<", "<").Replace(">", ">"); + + private static string ScalarString(JsonElement obj, string name) + { + if (!obj.TryGetProperty(name, out var v)) + { + return ""; + } + return v.ValueKind switch + { + JsonValueKind.String => v.GetString() ?? "", + JsonValueKind.Number => v.GetRawText(), + _ => "", + }; + } + + private static bool IsTruthy(JsonElement obj, string name) + { + if (!obj.TryGetProperty(name, out var v)) + { + return false; + } + return v.ValueKind switch + { + JsonValueKind.True => true, + JsonValueKind.String => !string.IsNullOrEmpty(v.GetString()), + JsonValueKind.Number => v.TryGetDouble(out var d) && d != 0, + JsonValueKind.Array => v.GetArrayLength() > 0, + JsonValueKind.Object => v.EnumerateObject().Any(), + _ => false, + }; + } +} diff --git a/services/FileService/tests/FileService.Tests/ApiIntegrationTests.cs b/services/FileService/tests/FileService.Tests/ApiIntegrationTests.cs index 1ae1185e..3c521a52 100644 --- a/services/FileService/tests/FileService.Tests/ApiIntegrationTests.cs +++ b/services/FileService/tests/FileService.Tests/ApiIntegrationTests.cs @@ -95,6 +95,66 @@ public async Task By_domain_workbook_resolves_report() Assert.Equal(HttpStatusCode.OK, response.StatusCode); } + [Fact] + public async Task Report_csv_returns_csv() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/v1/reports/1/csv?disposition=inline"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/csv", response.Content.Headers.ContentType?.MediaType); + var body = await response.Content.ReadAsStringAsync(); + Assert.Contains("# Site Audit export", body); + Assert.Contains("example.com", body); + } + + [Fact] + public async Task Report_json_returns_payload() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/v1/reports/1/json"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType?.MediaType); + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("example.com", doc.RootElement.GetProperty("site_name").GetString()); + } + + [Fact] + public async Task Report_sitemap_returns_xml() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/v1/reports/1/sitemap"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("application/xml", response.Content.Headers.ContentType?.MediaType); + var body = await response.Content.ReadAsStringAsync(); + Assert.Contains("> ListReportsAsync(CancellationToken cancellationToken = default) diff --git a/services/FileService/tests/FileService.Tests/ReportDataClientTests.cs b/services/FileService/tests/FileService.Tests/ReportDataClientTests.cs deleted file mode 100644 index cfb5783f..00000000 --- a/services/FileService/tests/FileService.Tests/ReportDataClientTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Net; -using System.Text.Json; -using FileService.Application.Clients; -using FileService.Application.Options; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; - -namespace FileService.Tests; - -public class ReportDataClientTests -{ - private static ReportDataClient CreateClient(HttpClient http) => - new(http, Options.Create(new ReportApiOptions { BaseUrl = "http://report-api.test", TimeoutSeconds = 30 }), - NullLogger.Instance); - - [Fact] - public async Task ListReportsAsync_parses_meta_response() - { - using var http = TestHttpHandler.CreateClient(_ => - TestHttpHandler.Json("""{"reports":[{"id":3,"canonical_domain":"ex.com","site_name":"Ex","generated_at":"2025-01-01"}]}""")); - var client = CreateClient(http); - - var rows = await client.ListReportsAsync(); - - Assert.Single(rows); - Assert.Equal(3, rows[0].Id); - Assert.Equal("ex.com", rows[0].CanonicalDomain); - } - - [Fact] - public async Task ListReportsAsync_returns_empty_when_reports_missing() - { - using var http = TestHttpHandler.CreateClient(_ => TestHttpHandler.Json("{}")); - var client = CreateClient(http); - - var rows = await client.ListReportsAsync(); - - Assert.Empty(rows); - } - - [Fact] - public async Task GetPayloadAsync_returns_payload_element() - { - using var http = TestHttpHandler.CreateClient(_ => - TestHttpHandler.Json("""{"payload":{"site_name":"Acme"}}""")); - var client = CreateClient(http); - - var payload = await client.GetPayloadAsync(9); - - Assert.NotNull(payload); - Assert.Equal("Acme", payload.Value.GetProperty("site_name").GetString()); - } - - [Fact] - public async Task GetPayloadAsync_returns_null_on_404() - { - using var http = TestHttpHandler.CreateClient(_ => new HttpResponseMessage(HttpStatusCode.NotFound)); - var client = CreateClient(http); - - var payload = await client.GetPayloadAsync(404); - - Assert.Null(payload); - } - - [Fact] - public async Task GetPayloadAsync_returns_null_when_payload_property_missing() - { - using var http = TestHttpHandler.CreateClient(_ => TestHttpHandler.Json("{}")); - var client = CreateClient(http); - - var payload = await client.GetPayloadAsync(1); - - Assert.Null(payload); - } - - [Fact] - public async Task ListReportsAsync_throws_on_upstream_error() - { - using var http = TestHttpHandler.CreateClient(_ => new HttpResponseMessage(HttpStatusCode.BadGateway) - { - Content = new StringContent("upstream down"), - }); - var client = CreateClient(http); - - var ex = await Assert.ThrowsAsync(() => client.ListReportsAsync()); - Assert.Contains("502", ex.Message); - } -} diff --git a/services/FileService/tests/FileService.Tests/ReportExportersTests.cs b/services/FileService/tests/FileService.Tests/ReportExportersTests.cs new file mode 100644 index 00000000..e65be250 --- /dev/null +++ b/services/FileService/tests/FileService.Tests/ReportExportersTests.cs @@ -0,0 +1,83 @@ +using System.Text.Json; +using FileService.Rendering.Exports; + +namespace FileService.Tests; + +public class ReportExportersTests +{ + private const string SampleJson = """ + { + "site_name": "Example", + "report_generated_at": "2026-01-01", + "report_meta": { "data_sources": ["Crawl", "Lighthouse"] }, + "links": [ + { "url": "https://example.com/", "status": 200, "title": "Home", "inlinks": 3, "word_count": 100 }, + { "url": "https://example.com/secret", "status": 200, "noindex": true }, + { "url": "https://example.com/missing", "status": 404 } + ], + "executive_summary": { + "summary": "Looks good", + "source": "ai_insights", + "priorities": ["Fix titles", "Add alt text"] + }, + "categories": [ + { + "name": "Link Health", + "issues": [ + { + "priority": "High", + "message": "Broken link", + "url": "https://example.com/x", + "recommendation": "Fix it", + "llm_recommendation": "Fix it now" + } + ] + } + ] + } + """; + + private static JsonElement Sample() + { + using var doc = JsonDocument.Parse(SampleJson); + return doc.RootElement.Clone(); + } + + [Fact] + public void Csv_renders_all_sections() + { + var csv = new ReportCsvExporter().Generate(Sample()); + + Assert.Contains("# Site Audit export", csv); + Assert.Contains("site_name,Example", csv); + Assert.Contains("Crawl, Lighthouse", csv); // data_sources joined (quoted) + Assert.Contains("https://example.com/,200,Home,3,100", csv); // links row + Assert.Contains("# Executive summary", csv); + Assert.Contains("source,AI insights", csv); // ai_insights -> label + Assert.Contains("summary,Looks good", csv); + Assert.Contains("priority_1,Fix titles", csv); + Assert.Contains("priority_2,Add alt text", csv); + // Legacy category name remapped (Link Health -> Links); distinct llm recommendation used. + Assert.Contains("Links,High,Broken link,https://example.com/x,Fix it now,Fix it now", csv); + Assert.Contains("\r\n", csv); // CRLF line endings (csv.writer parity) + } + + [Fact] + public void Sitemap_includes_only_indexable_2xx_urls() + { + var xml = new ReportSitemapExporter().Generate(Sample()); + + Assert.Contains("https://example.com/", xml); + Assert.DoesNotContain("secret", xml); // noindex excluded + Assert.DoesNotContain("missing", xml); // non-2xx excluded + } + + [Fact] + public void Json_roundtrips_payload() + { + var json = new ReportJsonExporter().Generate(Sample()); + using var doc = JsonDocument.Parse(json); + Assert.Equal("Example", doc.RootElement.GetProperty("site_name").GetString()); + } +} diff --git a/src/website_profiling/api/main.py b/src/website_profiling/api/main.py index e26b2d4d..48f97d2e 100644 --- a/src/website_profiling/api/main.py +++ b/src/website_profiling/api/main.py @@ -16,7 +16,6 @@ content, crawl, dashboards, - filters, health, integrations, issues, @@ -27,12 +26,9 @@ page_coach, page_markdown, pipeline, - portfolio, properties, report, report_audit_tool, - report_export, - report_portfolio, schedule, ) @@ -92,9 +88,8 @@ async def _lifespan(app: FastAPI) -> AsyncIterator[None]: # ── Batch F: Properties ────────────────────────────────────────────────────── app.include_router(properties.router, prefix="/api") -# ── Batch G: Dashboards + Filters ──────────────────────────────────────────── +# ── Batch G: Dashboards ────────────────────────────────────────────────────── app.include_router(dashboards.router, prefix="/api") -app.include_router(filters.router, prefix="/api") # ── Batch H: Google + Bing integrations ────────────────────────────────────── app.include_router(integrations.router, prefix="/api") @@ -106,12 +101,9 @@ async def _lifespan(app: FastAPI) -> AsyncIterator[None]: app.include_router(page_markdown.router, prefix="/api") app.include_router(ollama.router, prefix="/api") app.include_router(mcp_tools.router, prefix="/api") -app.include_router(portfolio.router, prefix="/api") app.include_router(alerts.router, prefix="/api") app.include_router(schedule.router, prefix="/api") app.include_router(logs.router, prefix="/api") app.include_router(compare.router, prefix="/api") app.include_router(page_coach.router, prefix="/api") app.include_router(report_audit_tool.router, prefix="/api") -app.include_router(report_export.router, prefix="/api") -app.include_router(report_portfolio.router, prefix="/api") diff --git a/src/website_profiling/api/routers/filters.py b/src/website_profiling/api/routers/filters.py deleted file mode 100644 index b2fb19d7..00000000 --- a/src/website_profiling/api/routers/filters.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Saved filters router — /api/filters""" -from __future__ import annotations - -from typing import Annotated, Any, Optional - -from fastapi import APIRouter, Depends, HTTPException, Query -from pydantic import BaseModel -from psycopg import Connection - -from ..deps import get_db -from website_profiling.db import saved_filter_store - -router = APIRouter(tags=["filters"]) - -DbDep = Annotated[Connection, Depends(get_db)] - - -class FilterUpsertBody(BaseModel): - propertyId: int - name: str - filterJson: Optional[Any] = None - - -class FilterDeleteBody(BaseModel): - propertyId: int - name: str - - -@router.get("/filters") -def list_filters( - conn: DbDep, - propertyId: int = Query(..., description="Property ID"), -) -> dict[str, Any]: - return {"filters": saved_filter_store.list_saved_filters(conn, propertyId)} - - -@router.post("/filters") -def upsert_filter(body: FilterUpsertBody, conn: DbDep) -> dict[str, Any]: - name = (body.name or "").strip() - if not body.propertyId or not name: - raise HTTPException(status_code=400, detail="propertyId and name required") - filter_json = body.filterJson if isinstance(body.filterJson, dict) else {} - saved_filter_store.upsert_saved_filter(conn, body.propertyId, name, filter_json) - return {"ok": True} - - -@router.delete("/filters") -def delete_filter(body: FilterDeleteBody, conn: DbDep) -> dict[str, Any]: - name = (body.name or "").strip() - if not body.propertyId or not name: - raise HTTPException(status_code=400, detail="propertyId and name required") - deleted = saved_filter_store.delete_saved_filter(conn, body.propertyId, name) - if not deleted: - raise HTTPException(status_code=404, detail="filter not found") - return {"ok": True} diff --git a/src/website_profiling/api/routers/integrations.py b/src/website_profiling/api/routers/integrations.py index c8a6fe1d..13bc2531 100644 --- a/src/website_profiling/api/routers/integrations.py +++ b/src/website_profiling/api/routers/integrations.py @@ -154,6 +154,41 @@ def google_disconnect(conn: DbDep) -> dict[str, Any]: } +# ── GET /api/integrations/google/auth ───────────────────────────────────────── +# OAuth consent + callback (moved server-side from the former Next.js routes; the browser +# reaches these through the BFF). Heavy logic lives in integrations/google/oauth.py. + +@router.get("/google/auth") +def google_oauth_start( + conn: DbDep, + propertyId: Optional[int] = Query(default=None), + startUrl: Optional[str] = Query(default=None), + returnTo: Optional[str] = Query(default=None), +) -> Any: + from fastapi.responses import RedirectResponse + from website_profiling.integrations.google.oauth import OAuthError, oauth_start + + try: + url = oauth_start(conn, propertyId, startUrl, returnTo) + except OAuthError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return RedirectResponse(url, status_code=302) + + +@router.get("/google/callback") +def google_oauth_callback( + conn: DbDep, + code: Optional[str] = Query(default=None), + state: Optional[str] = Query(default=None), + error: Optional[str] = Query(default=None), +) -> Any: + from fastapi.responses import RedirectResponse + from website_profiling.integrations.google.oauth import oauth_callback + + url = oauth_callback(conn, code, state, error) + return RedirectResponse(url, status_code=302) + + # ── GET /api/integrations/google/properties ─────────────────────────────────── @router.get("/google/properties") diff --git a/src/website_profiling/api/routers/issues.py b/src/website_profiling/api/routers/issues.py index 1f405ef8..35f461bf 100644 --- a/src/website_profiling/api/routers/issues.py +++ b/src/website_profiling/api/routers/issues.py @@ -1,67 +1,12 @@ """Issues routers — /api/issues/* and /api/ai/*.""" from __future__ import annotations -from typing import Annotated, Any +from typing import Any -from fastapi import APIRouter, Body, Depends, HTTPException, Query -from psycopg import Connection - -from ..deps import get_db -from website_profiling.db import issue_status_store +from fastapi import APIRouter, Body, HTTPException router = APIRouter(tags=["issues"]) -DbDep = Annotated[Connection, Depends(get_db)] - - -# ── GET /api/issues/status ──────────────────────────────────────────────────── - -@router.get("/issues/status") -def list_issue_status_route( - conn: DbDep, - propertyId: int = Query(...), -) -> dict[str, Any]: - if not propertyId: - raise HTTPException(status_code=400, detail="propertyId required") - return {"issues": issue_status_store.list_issue_status(conn, propertyId)} - - -# ── PUT /api/issues/status ──────────────────────────────────────────────────── - -@router.put("/issues/status") -def upsert_issue_status_route( - conn: DbDep, - body: dict[str, Any] = Body(default={}), -) -> dict[str, Any]: - property_id = int(body.get("propertyId") or 0) - message = str(body.get("message") or "").strip() - status = str(body.get("status") or "") - - if not property_id or not message or not status: - raise HTTPException( - status_code=400, - detail="propertyId, message, and valid status required", - ) - - report_id = body.get("reportId") - try: - issue = issue_status_store.upsert_issue_status( - conn, - property_id=property_id, - message=message, - status=status, - report_id=int(report_id) if report_id is not None else None, - url=str(body.get("url") or ""), - priority=str(body.get("priority") or "Medium"), - category_id=body.get("categoryId") or None, - assignee=body.get("assignee") or None, - note=body.get("note") or None, - ) - except ValueError as exc: - raise HTTPException(status_code=400, detail=str(exc)) from exc - - return {"issue": issue} - # ── POST /api/issues/fix-suggestion ────────────────────────────────────────── diff --git a/src/website_profiling/api/routers/portfolio.py b/src/website_profiling/api/routers/portfolio.py deleted file mode 100644 index ed0c8d76..00000000 --- a/src/website_profiling/api/routers/portfolio.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Portfolio item deletion — /api/portfolio/*.""" -from __future__ import annotations - -from typing import Annotated, Any, Optional - -from fastapi import APIRouter, Depends, HTTPException -from psycopg import Connection -from pydantic import BaseModel - -from ..deps import get_db -from website_profiling.db import portfolio_store - -router = APIRouter(tags=["portfolio"]) - -DbDep = Annotated[Connection, Depends(get_db)] - - -class DeletePortfolioBody(BaseModel): - reportId: Optional[int] = None - crawlRunId: Optional[int] = None - - -@router.delete("/portfolio/delete") -def delete_portfolio_item(body: DeletePortfolioBody, conn: DbDep) -> dict[str, Any]: - if body.reportId is None and body.crawlRunId is None: - raise HTTPException(status_code=400, detail="reportId or crawlRunId required") - - deleted = portfolio_store.delete_portfolio_item( - conn, - report_id=body.reportId, - crawl_run_id=body.crawlRunId, - ) - if not deleted: - raise HTTPException(status_code=404, detail="portfolio item not found") - return {"ok": True} diff --git a/src/website_profiling/api/routers/report.py b/src/website_profiling/api/routers/report.py index 507c6aaf..c99fb623 100644 --- a/src/website_profiling/api/routers/report.py +++ b/src/website_profiling/api/routers/report.py @@ -1,83 +1,7 @@ -"""Report data routers — /api/report/*.""" -from __future__ import annotations - -from typing import Annotated, Any, Optional - -from fastapi import APIRouter, Depends, HTTPException, Query -from psycopg import Connection - -from ..deps import get_db -from ..services.report_loader import ( - SECTION_KEYS, - get_crawl_preview_payload, - get_mobile_desktop_delta, - get_report_payload, - list_audit_history, - list_crawl_runs, - list_reports, -) +"""Report data routers — /api/report/* — ported to the .NET Data service.""" +# All routes previously here have been moved to services/Data. +# This file is intentionally empty; the router is kept only to avoid import errors +# in code that references report.router until the include_router call is removed. +from fastapi import APIRouter router = APIRouter(prefix="/report", tags=["report"]) - -DbDep = Annotated[Connection, Depends(get_db)] - - -@router.get("/meta") -def report_meta(conn: DbDep) -> dict[str, Any]: - return { - "reports": list_reports(conn), - "crawlRuns": list_crawl_runs(conn), - } - - -@router.get("/payload") -def report_payload( - conn: DbDep, - reportId: Optional[int] = Query(None), - domain: Optional[str] = Query(None), - section: Optional[str] = Query(None), -) -> dict[str, Any]: - if section is not None and section not in SECTION_KEYS: - raise HTTPException(status_code=400, detail="Invalid section") - payload = get_report_payload(conn, reportId, domain, section) - if payload is None: - raise HTTPException(status_code=404, detail="Report not found") - if section: - return {"payload": payload, "section": section} - return {"payload": payload} - - -@router.get("/history") -def report_history( - conn: DbDep, - propertyId: Optional[int] = Query(None), - domain: Optional[str] = Query(None), - limit: int = Query(20, ge=1, le=100), -) -> dict[str, Any]: - history = list_audit_history(conn, propertyId, domain, limit) - return {"history": history} - - -@router.get("/crawl-payload") -def crawl_payload( - conn: DbDep, - crawlRunId: Optional[int] = Query(None), -) -> dict[str, Any]: - if not crawlRunId or crawlRunId <= 0: - raise HTTPException(status_code=400, detail="Invalid crawlRunId") - try: - payload = get_crawl_preview_payload(conn, crawlRunId) - except ValueError as e: - raise HTTPException(status_code=404, detail=str(e)) - return {"payload": payload} - - -@router.get("/mobile-delta") -def mobile_delta( - conn: DbDep, - id: Optional[int] = Query(None), -) -> dict[str, Any]: - if not id: - raise HTTPException(status_code=400, detail="id required") - deltas = get_mobile_desktop_delta(conn, id) - return {"deltas": deltas} diff --git a/src/website_profiling/api/routers/report_export.py b/src/website_profiling/api/routers/report_export.py deleted file mode 100644 index 024548a0..00000000 --- a/src/website_profiling/api/routers/report_export.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Report export downloads — /api/report/export*.""" -from __future__ import annotations - -from typing import Annotated, Optional - -from fastapi import APIRouter, Depends, HTTPException, Query -from fastapi.responses import Response -from psycopg import Connection - -from ..deps import get_db - -router = APIRouter(prefix="/report", tags=["report-export"]) - -DbDep = Annotated[Connection, Depends(get_db)] - -EXPORT_FORMATS = {"csv", "json"} - - -@router.get("/export") -def export_report( - conn: DbDep, - format: str = Query("csv"), - reportId: Optional[int] = Query(None), -) -> Response: - if format not in EXPORT_FORMATS: - raise HTTPException(status_code=400, detail=f"Invalid format. Use one of {sorted(EXPORT_FORMATS)}") - - try: - if format == "csv": - from website_profiling.tools.export_audit import export_audit_csv as _export - content = _export(conn, reportId) - return Response( - content=content if isinstance(content, bytes) else content.encode(), - media_type="text/csv", - headers={"Content-Disposition": "attachment; filename=report.csv"}, - ) - if format == "json": - import json - from website_profiling.tools.export_audit import export_audit_json as _export - content = _export(conn, reportId) - body = json.dumps(content) if not isinstance(content, (str, bytes)) else content - return Response( - content=body if isinstance(body, bytes) else body.encode(), - media_type="application/json", - headers={"Content-Disposition": "attachment; filename=report.json"}, - ) - except ImportError as exc: - raise HTTPException(status_code=501, detail=f"Export module unavailable: {exc}") - except Exception as exc: - raise HTTPException(status_code=500, detail=str(exc)) - - raise HTTPException(status_code=500, detail="Export failed") - - -@router.get("/export-sitemap") -def export_sitemap( - conn: DbDep, - reportId: Optional[int] = Query(None), -) -> Response: - try: - from website_profiling.tools.export_sitemap import export_sitemap as _export - content = _export(conn, reportId) - return Response( - content=content if isinstance(content, bytes) else content.encode(), - media_type="application/xml", - headers={"Content-Disposition": "attachment; filename=sitemap.xml"}, - ) - except ImportError: - raise HTTPException(status_code=501, detail="Sitemap export unavailable") - except Exception as exc: - raise HTTPException(status_code=500, detail=str(exc)) - diff --git a/src/website_profiling/api/routers/report_portfolio.py b/src/website_profiling/api/routers/report_portfolio.py deleted file mode 100644 index 28dac5a0..00000000 --- a/src/website_profiling/api/routers/report_portfolio.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Portfolio report widget — GET /api/report/portfolio.""" -from __future__ import annotations - -from typing import Annotated, Any, Optional - -from fastapi import APIRouter, Depends, HTTPException, Query -from psycopg import Connection - -from ..deps import get_db -from ..services.portfolio_loader import get_portfolio_response - -router = APIRouter(prefix="/report", tags=["report-portfolio"]) - -DbDep = Annotated[Connection, Depends(get_db)] - - -@router.get("/portfolio") -def report_portfolio( - conn: DbDep, - widget: str = Query("full"), - ids: Optional[str] = Query(None), - reportId: Optional[int] = Query(None), - crawlRunId: Optional[int] = Query(None), -) -> dict[str, Any]: - """Return portfolio data — groups, crawl history, summary, or single card.""" - valid_widgets = {"full", "groups", "summary", "card"} - if widget not in valid_widgets: - raise HTTPException(status_code=400, detail="Invalid widget") - - if widget == "card" and reportId is None and crawlRunId is None: - raise HTTPException( - status_code=400, detail="reportId or crawlRunId required for card widget" - ) - - id_list: list[int] = [] - if ids: - for s in ids.split(","): - try: - n = int(s.strip()) - if n > 0: - id_list.append(n) - except ValueError: - pass - - try: - return get_portfolio_response( - conn, - widget=widget, - ids=id_list, - report_id=reportId, - crawl_run_id=crawlRunId, - ) - except Exception as exc: - raise HTTPException(status_code=500, detail=str(exc)) diff --git a/src/website_profiling/api/services/portfolio_loader.py b/src/website_profiling/api/services/portfolio_loader.py deleted file mode 100644 index c449c487..00000000 --- a/src/website_profiling/api/services/portfolio_loader.py +++ /dev/null @@ -1,606 +0,0 @@ -"""Portfolio grouping for /api/report/portfolio — port of web/src/lib/homePortfolio.ts.""" -from __future__ import annotations - -import re -from datetime import datetime -from typing import Any, Callable, Optional -from urllib.parse import urlparse - -from psycopg import Connection - -from website_profiling.db.report_store import read_report_payload - -from .report_loader import ( - list_crawl_run_summaries, - list_crawl_runs, - list_reports, - slice_payload_for_section, -) - -PORTFOLIO_CATEGORY_ORDER = ( - "technical_seo", - "performance", - "core_web_vitals", - "link_health", - "security", - "html_accessibility", - "mobile", - "intelligence", -) - -EMPTY_ISSUE_COUNTS = {"critical": 0, "high": 0, "medium": 0, "low": 0} - -DATA_SOURCE_IDS = frozenset({ - "crawl", - "lighthouse", - "search_console", - "analytics", - "backlinks", -}) - -UNKNOWN_BRAND = "Unknown property" -EM_DASH = "—" - - -def _extract_hostname(url: str | None) -> str: - if not url: - return "" - try: - host = urlparse(str(url)).hostname - return host.lower() if host else "" - except Exception: - return "" - - -def _slugify_domain(name: str | None) -> str: - if not name: - return "" - s = re.sub(r"[^a-z0-9]+", "-", str(name).strip().lower()).strip("-") - return s - - -def _canonical_domain_from_payload( - payload: dict[str, Any], - start_url_by_run_id: dict[int, str], -) -> str: - run_id = payload.get("crawl_run_id") - run_id = int(run_id) if run_id is not None else None - run_start = start_url_by_run_id.get(run_id, "") if run_id is not None else "" - top_pages = payload.get("top_pages") or [] - links = payload.get("links") or [] - fallback = "" - if top_pages and isinstance(top_pages[0], dict): - fallback = str(top_pages[0].get("url") or "") - if not fallback and links and isinstance(links[0], dict): - fallback = str(links[0].get("url") or "") - start_domain = _extract_hostname(run_start) - fallback_domain = _extract_hostname(fallback) - return (start_domain or fallback_domain or "").lower() - - -def _crawled_url_count(payload: dict[str, Any]) -> int: - scope = (payload.get("report_meta") or {}).get("crawl_scope") or {} - pages = scope.get("pages_crawled") - if pages is not None: - try: - n = int(pages) - if n > 0: - return n - except (TypeError, ValueError): - pass - summary = payload.get("summary") or {} - total = summary.get("total_urls") - if total is not None: - try: - n = int(total) - if n > 0: - return n - except (TypeError, ValueError): - pass - links = payload.get("links") or [] - return len(links) if links else 0 - - -def _score_from_categories(categories: list[dict[str, Any]]) -> int | None: - nums = [ - float(c["score"]) - for c in categories - if isinstance(c.get("score"), (int, float)) - ] - if not nums: - return None - return round(sum(nums) / len(nums)) - - -def _issue_counts_from_payload(payload: dict[str, Any]) -> tuple[dict[str, int], int]: - counts = dict(EMPTY_ISSUE_COUNTS) - for cat in payload.get("categories") or []: - for iss in cat.get("issues") or []: - p = str(iss.get("priority") or "Medium") - if p == "Critical": - counts["critical"] += 1 - elif p == "High": - counts["high"] += 1 - elif p == "Low": - counts["low"] += 1 - else: - counts["medium"] += 1 - total = sum(counts.values()) - return counts, total - - -def _category_score(payload: dict[str, Any], cat_id: str) -> int | None: - for cat in payload.get("categories") or []: - if cat.get("id") == cat_id and isinstance(cat.get("score"), (int, float)): - return round(float(cat["score"])) - return None - - -def _lh_scores(payload: dict[str, Any]) -> tuple[int | None, int | None]: - summary = payload.get("lighthouse_summary") - if not isinstance(summary, dict): - return None, None - mm = summary.get("median_metrics") or {} - cs = summary.get("category_scores") or {} - perf_raw = mm.get("performance_score") or cs.get("performance") - seo_raw = mm.get("seo_score") or cs.get("seo") - perf = round(float(perf_raw)) if isinstance(perf_raw, (int, float)) else None - seo = round(float(seo_raw)) if isinstance(seo_raw, (int, float)) else None - return perf, seo - - -def _category_snapshots(payload: dict[str, Any]) -> list[dict[str, Any]]: - cats = payload.get("categories") or [] - by_id = {str(c.get("id") or ""): c for c in cats} - out: list[dict[str, Any]] = [] - - def push(cat_id: str) -> None: - cat = by_id.get(cat_id) - if not cat or not isinstance(cat.get("score"), (int, float)): - return - out.append({ - "id": cat_id, - "name": str(cat.get("name") or cat_id), - "score": round(float(cat["score"])), - "issueCount": len(cat.get("issues") or []), - }) - - for cat_id in PORTFOLIO_CATEGORY_ORDER: - push(cat_id) - for cat in cats: - cat_id = str(cat.get("id") or "") - if not cat_id or any(r["id"] == cat_id for r in out): - continue - if not isinstance(cat.get("score"), (int, float)): - continue - out.append({ - "id": cat_id, - "name": str(cat.get("name") or cat_id), - "score": round(float(cat["score"])), - "issueCount": len(cat.get("issues") or []), - }) - return out - - -def _seo_signals(payload: dict[str, Any]) -> dict[str, int] | None: - s = payload.get("seo_health") - if not isinstance(s, dict): - return None - return { - "missingTitles": int(s.get("missing_title") or 0), - "missingMetaDesc": int(s.get("missing_meta_desc") or 0), - "thinContent": int(s.get("thin_content") or 0), - "h1Issues": int(s.get("h1_zero") or 0) + int(s.get("h1_multi") or 0), - } - - -def _median_word_count(payload: dict[str, Any]) -> int | None: - median = (payload.get("content_analytics") or {}).get("word_count_stats", {}).get("median") - return round(float(median)) if isinstance(median, (int, float)) else None - - -def _median_response_ms(payload: dict[str, Any]) -> int | None: - median = (payload.get("response_time_stats") or {}).get("p50") - return round(float(median)) if isinstance(median, (int, float)) else None - - -def _data_sources(payload: dict[str, Any]) -> list[str] | None: - raw = (payload.get("report_meta") or {}).get("data_sources") or [] - out = [str(s) for s in raw if str(s) in DATA_SOURCE_IDS] - return out or None - - -def _crawl_config_from_payload( - payload: dict[str, Any], - run_meta: dict[str, Any] | None, -) -> dict[str, Any] | None: - scope = (payload.get("report_meta") or {}).get("crawl_scope") - if not scope and not (run_meta or {}).get("render_mode") and not (run_meta or {}).get("discovery_mode"): - return None - cfg: dict[str, Any] = dict(scope) if isinstance(scope, dict) else {} - if run_meta: - if run_meta.get("render_mode") and "render_mode" not in cfg: - cfg["render_mode"] = run_meta["render_mode"] - if run_meta.get("discovery_mode"): - cfg["discovery_mode"] = run_meta["discovery_mode"] - return cfg or None - - -def _crawl_config_from_summary(row: dict[str, Any]) -> dict[str, Any] | None: - if not row.get("render_mode") and not row.get("discovery_mode") and not row.get("url_count"): - return None - return { - "pages_crawled": row.get("url_count"), - "render_mode": row.get("render_mode"), - "discovery_mode": row.get("discovery_mode"), - } - - -def _to_display_datetime(value: str | None) -> str: - if not value: - return "" - try: - if isinstance(value, datetime): - return value.isoformat() - dt = datetime.fromisoformat(str(value).replace("Z", "+00:00")) - return dt.isoformat() - except Exception: - return str(value) - - -def _generated_at_ms(value: str | None) -> float: - if not value: - return 0.0 - try: - return datetime.fromisoformat(str(value).replace("Z", "+00:00")).timestamp() * 1000 - except Exception: - return 0.0 - - -def _title_coverage_pct(with_title: int, url_count: int) -> int: - if url_count <= 0: - return 0 - return round((with_title / url_count) * 100) - - -def load_portfolio_maps(conn: Connection) -> dict[str, Any]: - crawl_rows = list_crawl_runs(conn) - start_url_by_run_id = {int(r["id"]): r["start_url"] for r in crawl_rows} - run_created_at_by_run_id = {int(r["id"]): r["created_at"] for r in crawl_rows} - run_meta_by_run_id = { - int(r["id"]): { - "render_mode": r.get("render_mode"), - "discovery_mode": r.get("discovery_mode"), - } - for r in crawl_rows - } - crawl_summaries = list_crawl_run_summaries(conn) - return { - "start_url_by_run_id": start_url_by_run_id, - "run_created_at_by_run_id": run_created_at_by_run_id, - "run_meta_by_run_id": run_meta_by_run_id, - "crawl_summaries": crawl_summaries, - } - - -def compute_domain_groups( - report_list: list[dict[str, Any]], - maps: dict[str, Any], - get_payload: Callable[[int], dict[str, Any] | None], -) -> list[dict[str, Any]]: - start_url_by_run_id: dict[int, str] = maps["start_url_by_run_id"] - run_created_at_by_run_id: dict[str, str] = maps["run_created_at_by_run_id"] - run_meta_by_run_id: dict[int, dict[str, Any]] = maps["run_meta_by_run_id"] - brand_map: dict[str, dict[str, Any]] = {} - - for r in report_list: - report_id = int(r["id"]) - payload = get_payload(report_id) - if not payload: - continue - - run_id = payload.get("crawl_run_id") - run_id_int = int(run_id) if run_id is not None else None - run_start_url = start_url_by_run_id.get(run_id_int, "") if run_id_int is not None else "" - top = payload.get("top_pages") or [] - links = payload.get("links") or [] - if top and isinstance(top[0], dict): - fallback_url = str(top[0].get("url") or "") - elif links and isinstance(links[0], dict): - fallback_url = str(links[0].get("url") or "") - else: - fallback_url = "" - crawl_url = (run_start_url or fallback_url or "").strip() - start_domain = _extract_hostname(run_start_url) - fallback_domain = _extract_hostname(crawl_url) - domain_name = start_domain or fallback_domain or str(payload.get("site_name") or UNKNOWN_BRAND) - brand_key = start_domain or (f"fallback:{fallback_domain}" if fallback_domain else f"report:{report_id}") - - summary = payload.get("summary") or {} - status_counts = { - "s2xx": int(summary.get("count_2xx") or 0), - "s3xx": int(summary.get("count_3xx") or 0), - "s4xx": int(summary.get("count_4xx") or 0), - "s5xx": int(summary.get("count_5xx") or 0), - "other": int(summary.get("count_error") or 0), - } - url_count = _crawled_url_count(payload) - success_pct = round((status_counts["s2xx"] / url_count) * 100) if url_count > 0 else 0 - health_score = _score_from_categories(payload.get("categories") or []) or 0 - run_created_at = run_created_at_by_run_id.get(run_id_int, "") if run_id_int is not None else "" - last_crawl = _to_display_datetime( - run_created_at or payload.get("crawl_run_created_at") or payload.get("report_generated_at") or r.get("generated_at") - ) - last_audit = _to_display_datetime(payload.get("report_generated_at") or r.get("generated_at")) - generated_at_ms = _generated_at_ms(r.get("generated_at")) - issue_counts, total_issues = _issue_counts_from_payload(payload) - perf_score, seo_score = _lh_scores(payload) - technical_seo_score = _category_score(payload, "technical_seo") - success_rate_raw = summary.get("success_rate") - success_rate = ( - round(float(success_rate_raw)) - if isinstance(success_rate_raw, (int, float)) - else (success_pct if url_count > 0 else None) - ) - crawl_duration_s = ( - round(float(summary["crawl_time_s"])) - if isinstance(summary.get("crawl_time_s"), (int, float)) - else None - ) - run_meta = run_meta_by_run_id.get(run_id_int) if run_id_int is not None else None - canonical_host = _canonical_domain_from_payload(payload, start_url_by_run_id) or _slugify_domain( - str(payload.get("site_name") or "") - ) - data_sources = _data_sources(payload) - - group = { - "domainName": domain_name, - "crawlUrl": crawl_url or EM_DASH, - "urlCount": url_count, - "healthScore": health_score, - "statusCounts": status_counts, - "lastCrawl": last_crawl, - "lastAudit": last_audit, - "totalIssues": total_issues, - "issueCounts": issue_counts, - "successRate": success_rate, - "titleCoverage": None, - "avgWordCount": None, - "thinPages": None, - "technicalSeoScore": technical_seo_score, - "perfScore": perf_score, - "seoScore": seo_score, - "crawlDurationS": crawl_duration_s, - "categorySnapshots": _category_snapshots(payload), - "seoSignals": _seo_signals(payload), - "securityFindings": len(payload.get("security_findings") or []), - "duplicateClusters": len(payload.get("content_duplicates") or []), - "medianWordCount": _median_word_count(payload), - "medianResponseMs": _median_response_ms(payload), - "reportId": report_id, - "crawlRunId": run_id_int, - "generatedAtMs": generated_at_ms, - "domainParam": canonical_host, - "crawlConfig": _crawl_config_from_payload(payload, run_meta), - "dataSources": data_sources, - } - - existing = brand_map.get(brand_key) - if not existing or generated_at_ms > existing["generatedAtMs"]: - brand_map[brand_key] = group - - return sorted(brand_map.values(), key=lambda g: g["generatedAtMs"], reverse=True) - - -def compute_crawl_only_groups( - crawl_summaries: list[dict[str, Any]], - report_groups: list[dict[str, Any]], -) -> list[dict[str, Any]]: - covered_domains = { - (g.get("domainParam") or _extract_hostname(g.get("crawlUrl")) or g.get("domainName", "")).lower() - for g in report_groups - if g.get("domainParam") or g.get("crawlUrl") or g.get("domainName") - } - covered_run_ids = { - int(g["crawlRunId"]) - for g in report_groups - if g.get("crawlRunId") is not None - } - - brand_map: dict[str, dict[str, Any]] = {} - for row in crawl_summaries: - crawl_run_id = int(row["crawl_run_id"]) - if crawl_run_id in covered_run_ids: - continue - start_url = str(row.get("start_url") or "").strip() - domain_name = _extract_hostname(start_url) or UNKNOWN_BRAND - domain_key = domain_name.lower() - if not domain_key or domain_key in covered_domains: - continue - - url_count = int(row.get("url_count") or 0) - with_title = int(row.get("with_title") or 0) - title_coverage = _title_coverage_pct(with_title, url_count) - avg_word_count = round(float(row.get("avg_word_count") or 0)) - thin_pages = int(row.get("thin_pages") or 0) - generated_at_ms = _generated_at_ms(row.get("created_at")) - - existing = brand_map.get(domain_key) - if existing and generated_at_ms <= existing["generatedAtMs"]: - continue - - brand_map[domain_key] = { - "domainName": domain_name, - "crawlUrl": start_url or EM_DASH, - "urlCount": url_count, - "healthScore": title_coverage, - "statusCounts": { - "s2xx": int(row.get("s2xx") or 0), - "s3xx": int(row.get("s3xx") or 0), - "s4xx": int(row.get("s4xx") or 0), - "s5xx": int(row.get("s5xx") or 0), - "other": int(row.get("other") or 0), - }, - "lastCrawl": _to_display_datetime(row.get("created_at")), - "lastAudit": "", - "totalIssues": 0, - "issueCounts": dict(EMPTY_ISSUE_COUNTS), - "successRate": None, - "titleCoverage": title_coverage, - "avgWordCount": avg_word_count, - "thinPages": thin_pages, - "technicalSeoScore": None, - "perfScore": None, - "seoScore": None, - "crawlDurationS": None, - "categorySnapshots": [], - "seoSignals": None, - "securityFindings": 0, - "duplicateClusters": 0, - "medianWordCount": avg_word_count or None, - "medianResponseMs": None, - "reportId": None, - "crawlRunId": crawl_run_id, - "crawlOnly": True, - "generatedAtMs": generated_at_ms, - "domainParam": domain_key, - "crawlConfig": _crawl_config_from_summary(row), - } - - return list(brand_map.values()) - - -def merge_portfolio_groups( - report_groups: list[dict[str, Any]], - crawl_only_groups: list[dict[str, Any]], -) -> list[dict[str, Any]]: - return sorted( - report_groups + crawl_only_groups, - key=lambda g: g["generatedAtMs"], - reverse=True, - ) - - -def build_crawl_history_by_domain( - summaries: list[dict[str, Any]], -) -> dict[str, list[dict[str, Any]]]: - by_domain: dict[str, list[dict[str, Any]]] = {} - for row in summaries: - key = _extract_hostname(row.get("start_url")) - if not key: - continue - pages = int(row.get("url_count") or 0) - point = { - "pagesDiscovered": pages, - "titleCoverage": _title_coverage_pct(int(row.get("with_title") or 0), pages), - "avgWordCount": round(float(row.get("avg_word_count") or 0)), - "createdAtMs": _generated_at_ms(row.get("created_at")), - } - by_domain.setdefault(key, []).append(point) - - out: dict[str, list[dict[str, Any]]] = {} - for key, points in by_domain.items(): - out[key] = sorted(points, key=lambda p: p["createdAtMs"])[-8:] - return out - - -def compute_portfolio_summary(groups: list[dict[str, Any]]) -> dict[str, Any]: - total_brands = len(groups) - total_urls = sum(int(g.get("urlCount") or 0) for g in groups) - avg_health = ( - round(sum(int(g.get("healthScore") or 0) for g in groups) / total_brands) - if total_brands - else None - ) - return {"totalBrands": total_brands, "totalUrls": total_urls, "avgHealth": avg_health} - - -def build_portfolio_card( - conn: Connection, - report_list: list[dict[str, Any]], - maps: dict[str, Any], - *, - report_id: int | None = None, - crawl_run_id: int | None = None, -) -> dict[str, Any] | None: - def get_full_payload(rid: int) -> dict[str, Any] | None: - return read_report_payload(conn, rid) - - if report_id is not None: - row = next((r for r in report_list if int(r["id"]) == report_id), None) - if not row: - return None - groups = compute_domain_groups([row], maps, get_full_payload) - return groups[0] if groups else None - - if crawl_run_id is not None: - report_groups = compute_domain_groups(report_list, maps, get_full_payload) - from_report = next((g for g in report_groups if g.get("crawlRunId") == crawl_run_id), None) - if from_report: - return from_report - summary = next( - (s for s in maps["crawl_summaries"] if int(s["crawl_run_id"]) == crawl_run_id), - None, - ) - if not summary: - return None - crawl_only = compute_crawl_only_groups([summary], report_groups) - return crawl_only[0] if crawl_only else None - - return None - - -def build_groups_bundle( - conn: Connection, - report_list: list[dict[str, Any]], - *, - lite: bool, -) -> dict[str, Any]: - maps = load_portfolio_maps(conn) - - def get_payload(rid: int) -> dict[str, Any] | None: - payload = read_report_payload(conn, rid) - if payload is None: - return None - return slice_payload_for_section(payload, "core") if lite else payload - - report_groups = compute_domain_groups(report_list, maps, get_payload) - crawl_only = compute_crawl_only_groups(maps["crawl_summaries"], report_groups) - groups = merge_portfolio_groups(report_groups, crawl_only) - crawl_history = build_crawl_history_by_domain(maps["crawl_summaries"]) - return {"groups": groups, "crawlHistoryByDomain": crawl_history} - - -def get_portfolio_response( - conn: Connection, - *, - widget: str, - ids: list[int], - report_id: int | None = None, - crawl_run_id: int | None = None, -) -> dict[str, Any]: - all_reports = list_reports(conn) - id_set = set(ids) - report_list = [r for r in all_reports if r["id"] in id_set] if ids else all_reports - - if widget == "card": - maps = load_portfolio_maps(conn) - group = build_portfolio_card( - conn, - report_list, - maps, - report_id=report_id, - crawl_run_id=crawl_run_id, - ) - return {"group": group} - - lite = widget in ("groups", "summary") - bundle = build_groups_bundle(conn, report_list, lite=lite) - - if widget == "summary": - return compute_portfolio_summary(bundle["groups"]) - - return { - "groups": bundle["groups"], - "crawlHistoryByDomain": bundle["crawlHistoryByDomain"], - } diff --git a/src/website_profiling/api/services/report_loader.py b/src/website_profiling/api/services/report_loader.py index 18dcd440..88e86e9c 100644 --- a/src/website_profiling/api/services/report_loader.py +++ b/src/website_profiling/api/services/report_loader.py @@ -1,64 +1,14 @@ -"""Report data loading service — DB queries for the /api/report/* routes.""" +"""Report data loading helpers retained for Python tests and tooling. + +Portfolio read routes (/api/report/portfolio) are served by the .NET Data service. +""" from __future__ import annotations -from typing import Any, Optional +from typing import Any from psycopg import Connection -from website_profiling.db._common import _parse_row_json, _row_field -from website_profiling.db.report_store import read_report_payload - -# ── Section slicing ───────────────────────────────────────────────────────── - -SECTION_FIELDS: dict[str, list[str]] = { - "core": [ - "site_name", "summary", "categories", "top_pages", "recommendations", - "seo_health", "social_coverage", "status_counts", "portfolio_benchmark", - "executive_summary", "crux_summary", "report_meta", "report_generated_at", - "crawl_only_preview", "crawl_run_id", "crawl_run_created_at", "site_level", - "ml_errors", - ], - "links": [ - "links", "link_edges", "link_rel_summary", "inlink_anchor_matrix", - "outbound_link_domains", "outlink_labels", "outlink_counts", - ], - "traffic": ["google"], - "keywords": [ - "keywords", "keyword_opportunities", "competitor_keyword_gap", - "semantic_keyword_clusters", - ], - "issues": ["issues", "redirects"], - "content": [ - "content_urls", "content_duplicates", "content_analytics", - "text_content_analysis", "response_time_stats", - ], - "lighthouse": [ - "lighthouse_summary", "lighthouse_by_url", "lighthouse_diagnostics", - "lighthouse_human_summary", - ], - "security": ["security_findings"], - "gsc-links": ["gsc_links", "bing_backlinks"], - "structure": ["graph_nodes", "graph_edges", "depth_distribution"], - "tech": ["tech_stack_summary", "subdomains", "contact_intelligence"], - "indexation": [ - "indexation_coverage", "hreflang_summary", "ner_site_summary", - "language_summary", "rich_results_validation", "url_fingerprints", - "rich_results_meta", - ], - "gallery": [ - "mime_labels", "mime_values", "title_labels", "title_counts", - "domain_labels", "domain_values", - ], -} - -SECTION_KEYS = list(SECTION_FIELDS.keys()) - - -def slice_payload_for_section( - payload: dict[str, Any], section: str -) -> dict[str, Any]: - fields = SECTION_FIELDS.get(section, []) - return {k: payload[k] for k in fields if k in payload} +from website_profiling.db._common import _row_field # ── Report list ────────────────────────────────────────────────────────────── @@ -80,6 +30,29 @@ def list_reports(conn: Connection) -> list[dict[str, Any]]: return result +def list_reports_latest_per_domain(conn: Connection) -> list[dict[str, Any]]: + """One row per property — used by portfolio home grouping.""" + cur = conn.execute( + """ + SELECT DISTINCT ON (COALESCE(NULLIF(canonical_domain, ''), site_name)) + id, canonical_domain, site_name, generated_at + FROM report_payload + ORDER BY COALESCE(NULLIF(canonical_domain, ''), site_name), generated_at DESC + """ + ) + rows = cur.fetchall() + result = [] + for row in rows: + generated = _row_field(row, "generated_at") + result.append({ + "id": int(_row_field(row, "id")), + "canonical_domain": _row_field(row, "canonical_domain"), + "site_name": _row_field(row, "site_name"), + "generated_at": generated.isoformat() if hasattr(generated, "isoformat") else generated, + }) + return result + + # ── Crawl runs ─────────────────────────────────────────────────────────────── def list_crawl_runs(conn: Connection) -> list[dict[str, Any]]: @@ -103,11 +76,20 @@ def list_crawl_runs(conn: Connection) -> list[dict[str, Any]]: return result -def list_crawl_run_summaries(conn: Connection) -> list[dict[str, Any]]: +def list_crawl_run_summaries(conn: Connection, *, max_runs: int | None = None) -> list[dict[str, Any]]: """Aggregate crawl run stats for portfolio cards and crawl history.""" try: - cur = conn.execute( + run_filter = "" + params: tuple[Any, ...] = () + if max_runs is not None and max_runs > 0: + run_filter = """ + WHERE cr.id IN ( + SELECT id FROM crawl_runs ORDER BY id DESC LIMIT %s + ) """ + params = (int(max_runs),) + cur = conn.execute( + f""" SELECT cr.id AS crawl_run_id, cr.start_url, @@ -134,9 +116,11 @@ def list_crawl_run_summaries(conn: Connection) -> list[dict[str, Any]]: )::int AS thin_pages FROM crawl_runs cr LEFT JOIN crawl_results crl ON crl.crawl_run_id = cr.id + {run_filter} GROUP BY cr.id, cr.start_url, cr.created_at, cr.render_mode, cr.discovery_mode ORDER BY cr.id DESC - """ + """, + params, ) rows = cur.fetchall() except Exception: @@ -161,222 +145,3 @@ def list_crawl_run_summaries(conn: Connection) -> list[dict[str, Any]]: "discovery_mode": _row_field(row, "discovery_mode"), }) return result - - -# ── Report payload ─────────────────────────────────────────────────────────── - -def get_report_payload( - conn: Connection, - report_id: Optional[int] = None, - domain: Optional[str] = None, - section: Optional[str] = None, -) -> Optional[dict[str, Any]]: - resolved_id = report_id - - if resolved_id is None and domain: - domain_lower = domain.strip().lower() - reports = list_reports(conn) - match = next( - (r for r in reports if (r.get("canonical_domain") or "").lower() == domain_lower), - None, - ) - if match: - resolved_id = match["id"] - - payload = read_report_payload(conn, resolved_id) - if payload is None: - return None - - if section and section in SECTION_FIELDS: - return slice_payload_for_section(payload, section) - return payload - - -# ── Crawl preview ──────────────────────────────────────────────────────────── - -def get_crawl_preview_payload(conn: Connection, crawl_run_id: int) -> dict[str, Any]: - cur = conn.execute( - "SELECT id, start_url, created_at FROM crawl_runs WHERE id = %s", - (crawl_run_id,), - ) - run_row = cur.fetchone() - if not run_row: - raise ValueError("Crawl run not found") - - start_url = str(_row_field(run_row, "start_url") or "") - from urllib.parse import urlparse - try: - site_host = urlparse(start_url).hostname or "" - except Exception: - site_host = "" - - cur2 = conn.execute( - "SELECT url, data FROM crawl_results WHERE crawl_run_id = %s", - (crawl_run_id,), - ) - pages = [] - for row in cur2.fetchall(): - data = _parse_row_json(row, "data", index=1) - if not isinstance(data, dict): - data = {} - pages.append({"url": str(_row_field(row, "url") or ""), **data}) - - return { - "crawl_only_preview": True, - "crawl_run_id": crawl_run_id, - "site_name": site_host, - "top_pages": pages, - } - - -# ── Audit history ──────────────────────────────────────────────────────────── - -def _avg_score(categories: list[dict[str, Any]]) -> Optional[int]: - nums = [float(c["score"]) for c in categories if isinstance(c.get("score"), (int, float))] - if not nums: - return None - return round(sum(nums) / len(nums)) - - -def _issue_counts(categories: list[dict[str, Any]]) -> dict[str, int]: - counts: dict[str, int] = {"Critical": 0, "High": 0, "Medium": 0, "Low": 0} - for cat in categories: - for issue in (cat.get("issues") or []): - p = str(issue.get("priority") or "Medium") - counts[p] = counts.get(p, 0) + 1 - return counts - - -def _lh_scores(payload: dict[str, Any]) -> tuple[Optional[int], Optional[int]]: - summary = payload.get("lighthouse_summary") - if not isinstance(summary, dict): - return None, None - mm = summary.get("median_metrics") or {} - cs = summary.get("category_scores") or {} - perf_raw = mm.get("performance_score") or cs.get("performance") - seo_raw = mm.get("seo_score") or cs.get("seo") - perf = round(float(perf_raw)) if isinstance(perf_raw, (int, float)) else None - seo = round(float(seo_raw)) if isinstance(seo_raw, (int, float)) else None - return perf, seo - - -def list_audit_history( - conn: Connection, - property_id: Optional[int] = None, - domain: Optional[str] = None, - limit: int = 20, -) -> list[dict[str, Any]]: - clauses: list[str] = [] - vals: list[Any] = [] - - if property_id is not None and property_id > 0: - clauses.append("property_id = %s") - vals.append(property_id) - elif domain: - normalized = domain.strip().lower() - clauses.append( - "(LOWER(canonical_domain) = %s OR regexp_replace(LOWER(COALESCE(canonical_domain, '')), '[^a-z0-9]+', '-', 'g') = %s)" - ) - vals.append(normalized) - vals.append(normalized) - - limit = max(1, min(100, limit)) - vals.append(limit) - where = f"WHERE {' AND '.join(clauses)}" if clauses else "" - - cur = conn.execute( - f"""SELECT id, canonical_domain, site_name, generated_at, data - FROM report_payload {where} - ORDER BY generated_at DESC LIMIT %s""", - vals, - ) - rows = cur.fetchall() - result = [] - for row in rows: - data = _parse_row_json(row, "data") - if not isinstance(data, dict): - data = {} - categories = data.get("categories") or [] - cat_scores = { - (c.get("id") or c.get("name") or "unknown"): float(c["score"]) - for c in categories - if isinstance(c.get("score"), (int, float)) - } - perf, seo = _lh_scores(data) - tech_seo_cat = next((c for c in categories if c.get("id") == "technical_seo"), None) - tech_seo = round(float(tech_seo_cat["score"])) if tech_seo_cat and isinstance(tech_seo_cat.get("score"), (int, float)) else None - generated_at = _row_field(row, "generated_at") - result.append({ - "reportId": int(_row_field(row, "id")), - "canonicalDomain": _row_field(row, "canonical_domain"), - "siteName": _row_field(row, "site_name"), - "generatedAt": generated_at.isoformat() if hasattr(generated_at, "isoformat") else generated_at, - "healthScore": _avg_score(categories), - "categoryScores": cat_scores, - "issueCounts": _issue_counts(categories), - "perfScore": perf, - "seoScore": seo, - "technicalSeoScore": tech_seo, - }) - return result - - -# ── Mobile-desktop delta ───────────────────────────────────────────────────── - -def get_mobile_desktop_delta(conn: Connection, run_id: int) -> list[dict[str, Any]]: - cur = conn.execute( - "SELECT mobile_run_id FROM crawl_runs WHERE id = %s", (run_id,) - ) - row = cur.fetchone() - mobile_run_id = _row_field(row, "mobile_run_id") - if not row or mobile_run_id is None: - return [] - mobile_run_id = int(mobile_run_id) - - def fetch_run(rid: int) -> dict[str, dict[str, Any]]: - c = conn.execute( - "SELECT url, data FROM crawl_results WHERE crawl_run_id = %s", (rid,) - ) - m: dict[str, dict[str, Any]] = {} - for r in c.fetchall(): - d = _parse_row_json(r, "data", index=1) - if not isinstance(d, dict): - d = {} - key = str(_row_field(r, "url") or "").rstrip("/").lower() - m[key] = { - "title": str(d.get("title") or ""), - "h1": str(d.get("h1") or ""), - "word_count": int(d.get("word_count") or 0), - "status": int(d.get("status") or 0), - } - return m - - desktop_map = fetch_run(run_id) - mobile_map = fetch_run(mobile_run_id) - - deltas = [] - for key, desktop in desktop_map.items(): - mobile = mobile_map.get(key) - if not mobile: - continue - title_differs = desktop["title"] != mobile["title"] - h1_differs = desktop["h1"] != mobile["h1"] - word_count_delta = abs(desktop["word_count"] - mobile["word_count"]) - status_differs = desktop["status"] != mobile["status"] - if not title_differs and not h1_differs and word_count_delta <= 50 and not status_differs: - continue - deltas.append({ - "url": key, - "desktop": desktop, - "mobile": mobile, - "title_differs": title_differs, - "h1_differs": h1_differs, - "word_count_delta": word_count_delta, - "status_differs": status_differs, - }) - - deltas.sort( - key=lambda d: (d["status_differs"] * 4 + d["title_differs"] * 2 + d["h1_differs"]), - reverse=True, - ) - return deltas diff --git a/src/website_profiling/content_analysis/tokenize.py b/src/website_profiling/content_analysis/tokenize.py index b5bc86db..28609d02 100644 --- a/src/website_profiling/content_analysis/tokenize.py +++ b/src/website_profiling/content_analysis/tokenize.py @@ -5,7 +5,11 @@ def tokenize_words(body_text: str) -> list[str]: - return [w for w in re.findall(r"[a-zA-Z]+", body_text or "") if len(w) >= 2] + # [^\W\d_] = any Unicode letter (word char that is not a digit or underscore), + # so accented/non-Latin scripts (café, München, 日本語) are kept intact. The + # old [a-zA-Z]+ silently dropped every non-ASCII letter, corrupting word + # counts, reading level, and keyword extraction for non-English content. + return [w for w in re.findall(r"[^\W\d_]+", body_text or "", re.UNICODE) if len(w) >= 2] def count_words(tokens: list[str]) -> int: diff --git a/src/website_profiling/crawl/crawler.py b/src/website_profiling/crawl/crawler.py index 1a1a3d9c..7114a08b 100644 --- a/src/website_profiling/crawl/crawler.py +++ b/src/website_profiling/crawl/crawler.py @@ -481,6 +481,32 @@ def crawl( desc="Pages", disable=not use_tqdm, ) + + def _collect_future(f, f_url) -> None: + """Append one completed future's result to self.results (and the DB + writer). Shared by the main loop and the pause-drain so both persist + in-flight work identically. Calling f.result() blocks until done.""" + nonlocal pages_crawled + try: + res = f.result() + except Exception: + # Keep the dequeued url so the error row is persisted to the DB + # consistently with the non-streaming path (an url-less row is + # silently dropped from streaming). + res = empty_crawl_row(url=f_url, status="error") + if self.store_outlinks: + res["outlink_targets"] = "[]" + self.results.append(res) + page_url = ( + str(res.get("url") or res.get("final_url") or "").strip() or None + ) + pages_crawled += 1 + if page_url and db_writer is not None: + db_writer.enqueue(res) + if use_tqdm: + pbar.update(1) + progress_tracker.maybe_emit(pages_crawled, page_url) + try: with ThreadPoolExecutor(max_workers=self.concurrency) as ex: while (len(self.results) < self.max_pages) and ( @@ -512,25 +538,7 @@ def crawl( remaining: dict = {} for f, f_url in futures.items(): if f.done(): - try: - res = f.result() - except Exception: - # Keep the dequeued url so the error row is persisted - # to the DB consistently with the non-streaming path - # (an url-less row is silently dropped from streaming). - res = empty_crawl_row(url=f_url, status="error") - if self.store_outlinks: - res["outlink_targets"] = "[]" - self.results.append(res) - page_url = ( - str(res.get("url") or res.get("final_url") or "").strip() or None - ) - pages_crawled += 1 - if page_url and db_writer is not None: - db_writer.enqueue(res) - if use_tqdm: - pbar.update(1) - progress_tracker.maybe_emit(pages_crawled, page_url) + _collect_future(f, f_url) else: remaining[f] = f_url futures = remaining @@ -549,6 +557,13 @@ def crawl( _PAUSE_EVENT.set() if _PAUSE_EVENT.is_set(): self.paused = True + # Drain in-flight futures before exiting so their results + # aren't lost. Their URLs are already marked visited, so a + # resumed crawl won't refetch them — collect them now (this + # blocks on each result()) or they vanish from results + DB. + for f, f_url in futures.items(): + _collect_future(f, f_url) + futures = {} break if self.queue.empty() and not futures: diff --git a/src/website_profiling/crawl/fetchers/browser.py b/src/website_profiling/crawl/fetchers/browser.py index bf2e2581..060d9467 100644 --- a/src/website_profiling/crawl/fetchers/browser.py +++ b/src/website_profiling/crawl/fetchers/browser.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +import logging import os import threading import time @@ -13,6 +14,8 @@ from .base import HEADER_KEYS, FetchResult from .browser_diagnostics import finalize_browser_diagnostics, truncate_diag_text +logger = logging.getLogger(__name__) + _BROWSER_INSTALL_MSG = ( "JavaScript crawl requires Playwright and Chromium. Install: " "pip install -r requirements.txt. " @@ -449,3 +452,8 @@ def close(self) -> None: self._loop.call_soon_threadsafe(self._jobs.put_nowait, None) if self._thread is not None and self._thread.is_alive(): self._thread.join(timeout=30) + if self._thread.is_alive(): # pragma: no cover - join-timeout path + logger.warning( + "BrowserFetcher event-loop thread did not exit within 30s; " + "the browser/Chromium process may not have shut down cleanly." + ) diff --git a/src/website_profiling/db/report_store.py b/src/website_profiling/db/report_store.py index 23df4228..f601feaf 100644 --- a/src/website_profiling/db/report_store.py +++ b/src/website_profiling/db/report_store.py @@ -44,8 +44,6 @@ def _write_audit_health_snapshot( report_data: dict[str, Any], ) -> None: """Persist health score row for portfolio sparklines and alerts.""" - import json - categories = report_data.get("categories") or [] scores = [ float(c.get("score")) @@ -80,8 +78,10 @@ def _write_audit_health_snapshot( report_id, canonical_domain or None, health_score, - json.dumps(category_scores), - json.dumps(issue_counts), + # Use the psycopg Json adapter (like report_data below) so these + # land as native jsonb objects, not double-encoded string literals. + _json_val(category_scores), + _json_val(issue_counts), ), ) @@ -124,3 +124,74 @@ def read_report_payload(conn: Connection, report_id: Optional[int] = None) -> Op return data if isinstance(data, dict) else None except Exception: return None + + +def read_report_payloads(conn: Connection, report_ids: list[int]) -> dict[int, dict[str, Any]]: + """Batch-load report JSON blobs — one round-trip for portfolio grouping.""" + if not report_ids: + return {} + try: + cur = conn.execute( + "SELECT id, data FROM report_payload WHERE id = ANY(%s)", + (report_ids,), + ) + out: dict[int, dict[str, Any]] = {} + for row in cur.fetchall(): + rid = _row_field(row, "id", index=0) + if rid is None: + continue + data = _parse_row_json(row, index=1) + if isinstance(data, dict): + out[int(rid)] = data + return out + except Exception: + return {} + + +def read_report_payloads_portfolio(conn: Connection, report_ids: list[int]) -> dict[int, dict[str, Any]]: + """Load only JSON fields needed for /api/report/portfolio — skips multi-MB link/LH blobs.""" + if not report_ids: + return {} + try: + cur = conn.execute( + """ + SELECT id, + jsonb_build_object( + 'site_name', data->'site_name', + 'summary', data->'summary', + 'categories', data->'categories', + 'top_pages', COALESCE(data->'top_pages', '[]'::jsonb), + 'report_meta', data->'report_meta', + 'report_generated_at', data->'report_generated_at', + 'crawl_run_id', data->'crawl_run_id', + 'crawl_run_created_at', data->'crawl_run_created_at', + 'lighthouse_summary', jsonb_build_object( + 'median_metrics', data->'lighthouse_summary'->'median_metrics', + 'category_scores', data->'lighthouse_summary'->'category_scores' + ), + 'seo_health', data->'seo_health', + 'content_analytics', jsonb_build_object( + 'word_count_stats', data->'content_analytics'->'word_count_stats' + ), + 'response_time_stats', jsonb_build_object( + 'p50', data->'response_time_stats'->'p50' + ), + 'security_findings', COALESCE(data->'security_findings', '[]'::jsonb), + 'content_duplicates', COALESCE(data->'content_duplicates', '[]'::jsonb) + ) AS data + FROM report_payload + WHERE id = ANY(%s) + """, + (report_ids,), + ) + out: dict[int, dict[str, Any]] = {} + for row in cur.fetchall(): + rid = _row_field(row, "id", index=0) + if rid is None: + continue + data = _parse_row_json(row, index=1) + if isinstance(data, dict): + out[int(rid)] = data + return out + except Exception: + return {} diff --git a/src/website_profiling/integrations/bing/webmaster.py b/src/website_profiling/integrations/bing/webmaster.py index b4578055..909c7dfc 100644 --- a/src/website_profiling/integrations/bing/webmaster.py +++ b/src/website_profiling/integrations/bing/webmaster.py @@ -36,25 +36,40 @@ def fetch_bing_backlinks_summary(api_key: str, site_url: str) -> dict[str, Any]: if not key or not site: return {"ok": False, "error": "Bing API key and site URL required", "source": "bing_webmaster"} - raw = _bing_json_get("GetLinkCounts", key, siteUrl=site, page=0) - if raw.get("error"): - return { - "ok": False, - "error": str(raw.get("error")), - "source": "bing_webmaster", - "site_url": site, - } - - payload = raw.get("d") if isinstance(raw.get("d"), dict) else raw - links = payload.get("Links") if isinstance(payload, dict) else [] + # GetLinkCounts is paginated. Walk every page (up to a safety cap) so a site + # with more than one page of backlinks isn't silently truncated to page 0. + _MAX_PAGES = 50 pages: list[dict[str, Any]] = [] - for row in links or []: - if not isinstance(row, dict): - continue - pages.append({ - "url": row.get("Url"), - "inbound_links": int(row.get("Count") or 0), - }) + total_pages = 1 + page = 0 + while page < _MAX_PAGES: + raw = _bing_json_get("GetLinkCounts", key, siteUrl=site, page=page) + if raw.get("error"): + if page == 0: + return { + "ok": False, + "error": str(raw.get("error")), + "source": "bing_webmaster", + "site_url": site, + } + break # keep pages already collected; stop on a later-page error + + payload = raw.get("d") if isinstance(raw.get("d"), dict) else raw + if isinstance(payload, dict): + total_pages = int(payload.get("TotalPages") or 1) + links = payload.get("Links") if isinstance(payload, dict) else [] + if not links: + break + for row in links: + if not isinstance(row, dict): + continue + pages.append({ + "url": row.get("Url"), + "inbound_links": int(row.get("Count") or 0), + }) + page += 1 + if page >= total_pages: + break total_inbound = sum(int(p.get("inbound_links") or 0) for p in pages) return { @@ -64,6 +79,6 @@ def fetch_bing_backlinks_summary(api_key: str, site_url: str) -> dict[str, Any]: "linked_pages": pages[:100], "linked_page_count": len(pages), "total_inbound_links": total_inbound, - "total_pages": int(payload.get("TotalPages") or 1) if isinstance(payload, dict) else 1, + "total_pages": total_pages, "provenance": "Bing Webmaster", } diff --git a/src/website_profiling/integrations/google/oauth.py b/src/website_profiling/integrations/google/oauth.py new file mode 100644 index 00000000..2e94398b --- /dev/null +++ b/src/website_profiling/integrations/google/oauth.py @@ -0,0 +1,198 @@ +""" +Google OAuth consent + callback, moved server-side (FastAPI) from the former Next.js routes. + +Stateless by design: the property id + return path + expiry are signed into the OAuth `state` +parameter (HMAC) rather than stored in cookies. This is what lets the flow work behind the .NET +BFF, which terminates auth and does not forward cookies to FastAPI. Google echoes `state` back on +the callback, so no server-side session is needed. + +This module lives under integrations/google/* which is intentionally omitted from the coverage +gates; the API router endpoints that call it stay thin. +""" +from __future__ import annotations + +import base64 +import hmac +import json +import os +import time +from hashlib import sha256 +from typing import Any +from urllib.parse import urlencode + +GOOGLE_AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth" +GOOGLE_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token" + +SCOPES = " ".join( + [ + "https://www.googleapis.com/auth/webmasters.readonly", + "https://www.googleapis.com/auth/analytics.readonly", + "https://www.googleapis.com/auth/adwords", + ] +) + +STATE_TTL_SECONDS = 600 # 10 minutes to complete consent + + +class OAuthError(Exception): + """Raised for bad input on the consent-start step (mapped to HTTP 400 by the router).""" + + +def _state_secret() -> str: + secret = (os.environ.get("AUTH_SECRET") or os.environ.get("SESSION_SECRET") or "").strip() + # Fall back to a fixed dev secret so local (auth-disabled) flows still round-trip. + return secret or "google-oauth-dev-state-secret" + + +def redirect_uri() -> str: + return ( + os.environ.get("GOOGLE_REDIRECT_URI") + or "http://localhost:8090/api/integrations/google/callback" + ) + + +def _app_base() -> str: + """Browser-facing app origin for the final redirect back into the UI.""" + return (os.environ.get("APP_PUBLIC_URL") or "http://localhost:3000").rstrip("/") + + +def _b64(raw: bytes) -> str: + return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=") + + +def _unb64(text: str) -> bytes: + pad = "=" * (-len(text) % 4) + return base64.urlsafe_b64decode(text + pad) + + +def sign_state(property_id: int, return_path: str, now: float | None = None) -> str: + payload = { + "p": int(property_id), + "r": return_path or "/", + "e": int((now if now is not None else time.time()) + STATE_TTL_SECONDS), + } + body = _b64(json.dumps(payload, separators=(",", ":")).encode("utf-8")) + sig = hmac.new(_state_secret().encode("utf-8"), body.encode("ascii"), sha256).hexdigest() + return f"{body}.{sig}" + + +def verify_state(state: str | None, now: float | None = None) -> dict[str, Any] | None: + if not state or "." not in state: + return None + body, _, sig = state.partition(".") + expected = hmac.new(_state_secret().encode("utf-8"), body.encode("ascii"), sha256).hexdigest() + if not hmac.compare_digest(sig, expected): + return None + try: + payload = json.loads(_unb64(body).decode("utf-8")) + except (ValueError, json.JSONDecodeError): + return None + if int(payload.get("e", 0)) < int(now if now is not None else time.time()): + return None + return payload + + +def _safe_return_path(raw: str | None) -> str: + """Only allow same-origin relative paths (avoid open redirects).""" + if not raw or not raw.startswith("/") or raw.startswith("//"): + return "/" + return raw + + +def build_consent_url(client_id: str, state: str) -> str: + params = { + "client_id": client_id, + "redirect_uri": redirect_uri(), + "response_type": "code", + "scope": SCOPES, + "access_type": "offline", + "prompt": "consent", + "include_granted_scopes": "true", + "state": state, + } + return f"{GOOGLE_AUTH_ENDPOINT}?{urlencode(params)}" + + +def _exchange_code(code: str, client_id: str, client_secret: str) -> str | None: + import requests + + resp = requests.post( + GOOGLE_TOKEN_ENDPOINT, + data={ + "code": code, + "client_id": client_id, + "client_secret": client_secret, + "redirect_uri": redirect_uri(), + "grant_type": "authorization_code", + }, + timeout=15, + ) + data = resp.json() if resp.content else {} + if not resp.ok: + return None + return data.get("refresh_token") + + +def _ui_redirect(return_path: str, params: dict[str, str]) -> str: + sep = "&" if "?" in return_path else "?" + return f"{_app_base()}{return_path}{sep}{urlencode(params)}" + + +def oauth_start(conn: Any, property_id: int | None, start_url: str | None, return_to: str | None) -> str: + """Resolve the property, build the Google consent URL. Raises OAuthError on bad input.""" + from ...db.google_app_store import app_client_credentials + from ...db.property_store import resolve_property_id_from_start_url + + pid = property_id + if pid is None and start_url: + pid = resolve_property_id_from_start_url(conn, start_url.strip()) + if pid is None or pid <= 0: + raise OAuthError("propertyId is required. Set Site URL and connect from Integrations.") + + client_id, _client_secret = app_client_credentials() + if not client_id: + raise OAuthError("Google client ID missing. Complete Step 1 in Integrations.") + + state = sign_state(pid, _safe_return_path(return_to)) + return build_consent_url(client_id, state) + + +def oauth_callback(conn: Any, code: str | None, state: str | None, error: str | None) -> str: + """Validate state, exchange the code, persist the refresh token. Always returns a UI redirect URL.""" + from ...db.google_app_store import app_client_credentials + from ...db.property_store import apply_property_google_credentials_patch + + payload = verify_state(state) + return_path = _safe_return_path(payload.get("r") if payload else None) + + if error: + return _ui_redirect(return_path, {"integrations": "open", "auth": "error", "reason": error}) + if payload is None: + return _ui_redirect( + return_path, + {"integrations": "open", "auth": "error", "reason": "Invalid or expired state."}, + ) + if not code: + return _ui_redirect( + return_path, + {"integrations": "open", "auth": "error", "reason": "No authorization code received."}, + ) + + client_id, client_secret = app_client_credentials() + if not client_id or not client_secret: + return _ui_redirect( + return_path, + {"integrations": "open", "auth": "error", "reason": "Client credentials missing."}, + ) + + refresh_token = _exchange_code(code, client_id, client_secret) + if not refresh_token: + return _ui_redirect( + return_path, + {"integrations": "open", "auth": "error", "reason": "Token exchange failed."}, + ) + + apply_property_google_credentials_patch( + conn, int(payload["p"]), refresh_token=refresh_token, auth_mode="oauth" + ) + return _ui_redirect(return_path, {"integrations": "open", "auth": "success"}) diff --git a/src/website_profiling/llm/providers/gemini.py b/src/website_profiling/llm/providers/gemini.py index c692d8b1..aa4a01a0 100644 --- a/src/website_profiling/llm/providers/gemini.py +++ b/src/website_profiling/llm/providers/gemini.py @@ -26,7 +26,9 @@ def complete_json(self, system: str, user: str) -> dict[str, Any]: "generationConfig": {"responseMimeType": "application/json", "temperature": 0.2}, } with httpx.Client(timeout=self._timeout) as client: - r = client.post(url, params={"key": self._api_key}, json=payload) + # Pass the key in a header, not the query string: URL query params are + # logged by proxies / access logs / monitoring, leaking the API key. + r = client.post(url, headers={"x-goog-api-key": self._api_key}, json=payload) r.raise_for_status() data = r.json() text = "" diff --git a/src/website_profiling/llm/providers/ollama.py b/src/website_profiling/llm/providers/ollama.py index 94b599c5..581c5154 100644 --- a/src/website_profiling/llm/providers/ollama.py +++ b/src/website_profiling/llm/providers/ollama.py @@ -228,7 +228,9 @@ def _stream_chat( content_parts.append(text) on_token(text) if msg.get("tool_calls"): - tool_calls = self._parse_tool_calls(msg["tool_calls"]) + # Accumulate across chunks — assigning here dropped any + # tool calls delivered in an earlier streamed chunk. + tool_calls.extend(self._parse_tool_calls(msg["tool_calls"])) if chunk.get("done"): break diff --git a/src/website_profiling/reporting/categories/mobile.py b/src/website_profiling/reporting/categories/mobile.py index 065dc97f..c5e5771a 100644 --- a/src/website_profiling/reporting/categories/mobile.py +++ b/src/website_profiling/reporting/categories/mobile.py @@ -40,16 +40,17 @@ def category_mobile(df: pd.DataFrame) -> dict: recommendation="Add .", )) deductions.append((min(25, int(no_viewport) * 5), True)) - viewport_content = success_df["viewport_content"].fillna("").astype(str) - viewport_ok = success_df["viewport_present"].astype(str).str.lower().isin(("true", "1", "yes")) - invalid = (viewport_content.str.strip().eq("") | (~viewport_content.str.contains("width|device-width", case=False, na=False))) & viewport_ok - if invalid.sum() > 0: - issues.append(_issue( - "Some pages have viewport without width or device-width.", - priority="High", - recommendation="Use content='width=device-width, initial-scale=1' (or similar).", - )) - deductions.append((10, True)) + if "viewport_content" in success_df.columns: + viewport_content = success_df["viewport_content"].fillna("").astype(str) + viewport_ok = success_df["viewport_present"].astype(str).str.lower().isin(("true", "1", "yes")) + invalid = (viewport_content.str.strip().eq("") | (~viewport_content.str.contains("width|device-width", case=False, na=False))) & viewport_ok + if invalid.sum() > 0: + issues.append(_issue( + "Some pages have viewport without width or device-width.", + priority="High", + recommendation="Use content='width=device-width, initial-scale=1' (or similar).", + )) + deductions.append((10, True)) score = _score_deductions(100, deductions) return { diff --git a/src/website_profiling/security_scanner.py b/src/website_profiling/security_scanner.py index 7ba22288..3844f47b 100644 --- a/src/website_profiling/security_scanner.py +++ b/src/website_profiling/security_scanner.py @@ -361,9 +361,13 @@ def _active_checks( if r.status_code != 200: continue text = (r.text or "").lower() - if xss_token.lower() in text: - # Check if unescaped (dangerous) - if f">{xss_token}<" in text or f'">{xss_token}<' in text or f"'{xss_token}'" in text: + token_l = xss_token.lower() + if token_l in text: + # Check if unescaped (dangerous). Compare lowercased + # token against the lowercased body — using the + # mixed-case token here never matched, so reflected + # XSS was silently missed. + if f">{token_l}<" in text or f'">{token_l}<' in text or f"'{token_l}'" in text: findings.append(_finding( "xss_reflected", "High", @@ -403,13 +407,11 @@ def _active_checks( # SQLi error-based: single quote in common param for pname in ["id", "page", "q", "search", "query", "cat"][:2]: - if pname not in params and not params: - test_params = {pname: ["'"]} - elif pname in params: - test_params = dict(params) - test_params[pname] = ["'"] - else: - continue + # Inject the probe into the existing params (or add it if absent). + # The old `and not params` guard skipped the test whenever the URL + # already had other query params, gutting SQLi coverage. + test_params = dict(params) + test_params[pname] = ["'"] new_query = urlencode(test_params, doseq=True) probe_url = urlunparse((parsed_u.scheme, parsed_u.netloc, path, parsed_u.params, new_query, "")) try: diff --git a/src/website_profiling/tools/audit_tools/core/sql_query.py b/src/website_profiling/tools/audit_tools/core/sql_query.py index 6ee26daf..9c9d5d53 100644 --- a/src/website_profiling/tools/audit_tools/core/sql_query.py +++ b/src/website_profiling/tools/audit_tools/core/sql_query.py @@ -71,8 +71,10 @@ "log_file_uploads", # LLM response cache "llm_cache", - # Properties (name/domain — useful for joins) - "properties", + # NOTE: `properties` is intentionally NOT allowlisted — it stores + # google_refresh_token and other OAuth credentials (see property_store.py), + # which must never be reachable from the chat SQL surface. It is also in + # _SECRET_TABLES below as a belt-and-suspenders fast reject. }) # --------------------------------------------------------------------------- @@ -198,6 +200,8 @@ "chat_sessions", "chat_messages", "content_drafts", + # Holds google_refresh_token / OAuth credentials — never queryable via chat. + "properties", }) _SECRET_TABLE_RE: re.Pattern[str] = re.compile( r"\b(" + "|".join(re.escape(t) for t in sorted(_SECRET_TABLES)) + r")\b", diff --git a/src/website_profiling/worker/loop.py b/src/website_profiling/worker/loop.py index 39f9b0fb..7e5b04c5 100644 --- a/src/website_profiling/worker/loop.py +++ b/src/website_profiling/worker/loop.py @@ -17,11 +17,14 @@ _POLL_INTERVAL = float(os.getenv("WP_WORKER_POLL_INTERVAL", "1.0")) _running = True +_shutdown_signum: int | None = None def _handle_sigterm(signum: int, frame: object) -> None: - global _running - logger.info("Worker received signal %s, shutting down after current job.", signum) + global _running, _shutdown_signum + # Signal handlers must not log — logging is not reentrant-safe on stderr. + if _shutdown_signum is None: + _shutdown_signum = signum _running = False @@ -49,4 +52,9 @@ def run_worker_loop() -> None: else: time.sleep(_POLL_INTERVAL) + if _shutdown_signum is not None: + logger.info( + "Worker received signal %s, shutting down after current job.", + _shutdown_signum, + ) logger.info("Worker exiting cleanly.") diff --git a/tests/api/test_api_integration.py b/tests/api/test_api_integration.py index 83d238af..ec403240 100644 --- a/tests/api/test_api_integration.py +++ b/tests/api/test_api_integration.py @@ -2,6 +2,9 @@ These catch response-shape regressions and dict_row bugs that unit tests miss. Requires DATABASE_URL (same as other @pytest.mark.integration tests). + +Report read routes (/api/report/meta, /api/report/payload, …) are served by the +Data service — see services/Data/tests/Data.Tests/ApiIntegrationTests.cs. """ from __future__ import annotations @@ -26,20 +29,6 @@ def test_health(api_client: TestClient) -> None: assert body["database"] == "up" -def test_report_meta_response_shape(api_client: TestClient) -> None: - res = api_client.get("/api/report/meta") - assert res.status_code == 200 - body = res.json() - assert "reports" in body - assert "crawlRuns" in body - assert isinstance(body["reports"], list) - for row in body["reports"]: - assert "canonical_domain" in row - assert "site_name" in row - assert "generated_at" in row - assert "canonicalDomain" not in row - - def test_properties_crud_and_ops(api_client: TestClient) -> None: domain = f"api-prop-{uuid.uuid4().hex[:10]}.example" create = api_client.post( @@ -211,88 +200,6 @@ def test_dashboards_crud(api_client: TestClient, test_property: dict[str, Any]) assert deleted.json()["ok"] is True -def test_saved_filters_crud(api_client: TestClient, test_property: dict[str, Any]) -> None: - property_id = int(test_property["id"]) - filter_name = f"filter-{uuid.uuid4().hex[:8]}" - - upsert = api_client.post( - "/api/filters", - json={ - "propertyId": property_id, - "name": filter_name, - "filterJson": {"status": ["200"]}, - }, - ) - assert upsert.status_code == 200 - assert upsert.json()["ok"] is True - - listed = api_client.get("/api/filters", params={"propertyId": property_id}) - assert listed.status_code == 200 - names = {f["name"] for f in listed.json()["filters"]} - assert filter_name in names - - deleted = api_client.request( - "DELETE", - "/api/filters", - json={"propertyId": property_id, "name": filter_name}, - ) - assert deleted.status_code == 200 - assert deleted.json()["ok"] is True - - -def test_issue_status_upsert_and_list(api_client: TestClient, test_property: dict[str, Any]) -> None: - property_id = int(test_property["id"]) - - empty = api_client.get("/api/issues/status", params={"propertyId": property_id}) - assert empty.status_code == 200 - assert isinstance(empty.json()["issues"], list) - - upsert = api_client.put( - "/api/issues/status", - json={ - "propertyId": property_id, - "message": "Missing meta description", - "status": "open", - "url": "https://example.com/page", - "priority": "Medium", - }, - ) - assert upsert.status_code == 200 - issue = upsert.json()["issue"] - assert issue["propertyId"] == property_id - assert issue["status"] == "open" - assert issue["message"] == "Missing meta description" - - listed = api_client.get("/api/issues/status", params={"propertyId": property_id}) - assert listed.status_code == 200 - messages = {i["message"] for i in listed.json()["issues"]} - assert "Missing meta description" in messages - - -def test_portfolio_delete_crawl_run(api_client: TestClient, test_property: dict[str, Any]) -> None: - property_id = int(test_property["id"]) - with db_session() as conn: - from website_profiling.db.crawl_store import create_crawl_run - - crawl_run_id = create_crawl_run( - conn, - start_url=f"https://{test_property['domain']}", - property_id=property_id, - ) - - res = api_client.request( - "DELETE", - "/api/portfolio/delete", - json={"crawlRunId": crawl_run_id}, - ) - assert res.status_code == 200 - assert res.json()["ok"] is True - - with db_session() as conn: - cur = conn.execute("SELECT id FROM crawl_runs WHERE id = %s", (crawl_run_id,)) - assert cur.fetchone() is None - - def test_properties_resolve(api_client: TestClient, test_property: dict[str, Any]) -> None: res = api_client.get( "/api/properties/resolve", @@ -350,8 +257,3 @@ def test_backlinks_velocity_empty(api_client: TestClient, test_property: dict[st ) assert res.status_code == 200 assert isinstance(res.json()["snapshots"], list) - - -def test_report_payload_not_found(api_client: TestClient) -> None: - res = api_client.get("/api/report/payload", params={"reportId": 999999999}) - assert res.status_code == 404 diff --git a/tests/api/test_google_oauth.py b/tests/api/test_google_oauth.py new file mode 100644 index 00000000..c8310160 --- /dev/null +++ b/tests/api/test_google_oauth.py @@ -0,0 +1,71 @@ +"""Google OAuth router endpoints (consent + callback). Heavy logic is in the +coverage-omitted integrations/google/oauth.py; here we cover the thin router lines +and the stateless-state signing.""" +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +from fastapi.testclient import TestClient + +from website_profiling.api.deps import get_db +from website_profiling.api.main import app +from website_profiling.integrations.google import oauth as oauth_mod +from website_profiling.integrations.google.oauth import OAuthError + + +def _fake_db(): + yield MagicMock() + + +def _client() -> TestClient: + app.dependency_overrides[get_db] = _fake_db + return TestClient(app) + + +def teardown_function() -> None: + app.dependency_overrides.clear() + + +def test_google_oauth_start_redirects_to_consent() -> None: + url = "https://accounts.google.com/o/oauth2/v2/auth?client_id=x&state=y" + with patch("website_profiling.integrations.google.oauth.oauth_start", return_value=url): + resp = _client().get( + "/api/integrations/google/auth?propertyId=1", follow_redirects=False + ) + assert resp.status_code == 302 + assert resp.headers["location"] == url + + +def test_google_oauth_start_bad_input_returns_400() -> None: + with patch( + "website_profiling.integrations.google.oauth.oauth_start", + side_effect=OAuthError("propertyId is required."), + ): + resp = _client().get("/api/integrations/google/auth", follow_redirects=False) + assert resp.status_code == 400 + assert resp.json()["detail"] == "propertyId is required." + + +def test_google_oauth_callback_redirects_to_ui() -> None: + url = "http://localhost:3000/?integrations=open&auth=success" + with patch("website_profiling.integrations.google.oauth.oauth_callback", return_value=url): + resp = _client().get( + "/api/integrations/google/callback?code=abc&state=s", follow_redirects=False + ) + assert resp.status_code == 302 + assert resp.headers["location"] == url + + +def test_oauth_state_roundtrip_tamper_and_expiry() -> None: + state = oauth_mod.sign_state(42, "/integrations", now=1000) + payload = oauth_mod.verify_state(state, now=1001) + assert payload is not None + assert payload["p"] == 42 + assert payload["r"] == "/integrations" + # Tampered signature → rejected. + assert oauth_mod.verify_state(state + "x", now=1001) is None + # Expired → rejected. + assert oauth_mod.verify_state(state, now=10_000) is None + # Missing/garbage → rejected. + assert oauth_mod.verify_state(None) is None + assert oauth_mod.verify_state("no-dot") is None diff --git a/tests/db_test_fakes.py b/tests/db_test_fakes.py index 63bfe8ff..f5fe1072 100644 --- a/tests/db_test_fakes.py +++ b/tests/db_test_fakes.py @@ -12,9 +12,16 @@ class FakeCursor: - def __init__(self, *, fetchone_value: Any = None, fetchall_value: list[Any] | None = None) -> None: + def __init__( + self, + *, + fetchone_value: Any = None, + fetchall_value: list[Any] | None = None, + rowcount: int = 0, + ) -> None: self._fetchone_value = fetchone_value self._fetchall_value = fetchall_value or [] + self.rowcount = rowcount self.executed: list[tuple[str, tuple[Any, ...] | None]] = [] self.executemany_calls: list[tuple[str, list[Any]]] = [] diff --git a/tests/test_bing_webmaster.py b/tests/test_bing_webmaster.py index f74d86a2..f319efe2 100644 --- a/tests/test_bing_webmaster.py +++ b/tests/test_bing_webmaster.py @@ -37,6 +37,20 @@ def test_fetch_bing_backlinks_summary_handles_api_error(mock_get) -> None: assert "Invalid API key" in result["error"] +@patch("website_profiling.integrations.bing.webmaster._bing_json_get") +def test_fetch_bing_backlinks_summary_paginates(mock_get) -> None: + # Regression: previously only page 0 was fetched, truncating multi-page results. + mock_get.side_effect = [ + {"d": {"Links": [{"Url": "https://example.com/a", "Count": 3}], "TotalPages": 2}}, + {"d": {"Links": [{"Url": "https://example.com/b", "Count": 1}], "TotalPages": 2}}, + ] + result = fetch_bing_backlinks_summary("key", "https://example.com") + assert result["ok"] is True + assert result["linked_page_count"] == 2 + assert result["total_inbound_links"] == 4 + assert mock_get.call_count == 2 + + @patch("website_profiling.integrations.bing.webmaster._bing_json_get") def test_fetch_bing_backlinks_summary_empty_links(mock_get) -> None: mock_get.return_value = {"d": {"Links": [], "TotalPages": 0}} diff --git a/tests/test_content_analysis.py b/tests/test_content_analysis.py index c1fee8a6..46420cf7 100644 --- a/tests/test_content_analysis.py +++ b/tests/test_content_analysis.py @@ -57,3 +57,14 @@ def test_tokenize_words_min_length() -> None: tokens = tokenize_words("I am ok testing") assert "I" not in tokens assert "am" in tokens + + +def test_tokenize_words_keeps_non_ascii_letters() -> None: + # Regression: [a-zA-Z]+ dropped accented/non-Latin letters (café -> "caf"). + tokens = tokenize_words("La café est très bon München 日本語") + assert "café" in tokens + assert "très" in tokens + assert "München" in tokens + assert "日本語" in tokens + # digits are still excluded (unchanged from the letters-only behaviour) + assert tokenize_words("abc123 456") == ["abc"] diff --git a/tests/test_crawl_pause_resume.py b/tests/test_crawl_pause_resume.py index 55f26319..0fb0fd9b 100644 --- a/tests/test_crawl_pause_resume.py +++ b/tests/test_crawl_pause_resume.py @@ -296,6 +296,78 @@ def serialize_state(self): mod._PAUSE_EVENT.clear() +def test_pause_drains_inflight_futures(tmp_path, monkeypatch): + """In-flight futures must be collected on pause, not silently dropped. + + Regression: previously the loop broke immediately on pause, abandoning + futures that were still running. Their URLs are already marked visited, so a + resumed crawl never refetches them — they vanished from results and the DB. + """ + import time + + import website_profiling.crawl.crawler as mod + from website_profiling.crawl.schema import empty_crawl_row + + monkeypatch.setenv("TMPDIR", str(tmp_path)) + mod._PAUSE_EVENT.clear() + + pid = os.getpid() + flag = tmp_path / f"wp_pause_{pid}.flag" + flag.write_text("") # pause requested before the crawl starts + + class _FakeFrontier: + def __init__(self, *a, **kw): + self.queue: Queue = Queue() + self.visited: set = set() + self.depths: dict = {} + self.lock = threading.Lock() + self.rp = None + self.queue.put("https://example.com/fast") + self.queue.put("https://example.com/slow") + + def should_skip_dequeued(self, url): + return False + + def mark_visited(self, url): + if url in self.visited: + return False + self.visited.add(url) + return True + + def seed_initial_urls(self, **kw): + pass + + def serialize_state(self): + return {"pending": [], "visited": [], "depths": {}} + + def _worker(url): + # The "slow" page is still in-flight when the "fast" future completes and + # the pause is detected, so it must be drained rather than dropped. + if url.endswith("/slow"): + time.sleep(0.4) + row = empty_crawl_row(status=200) + row["url"] = url + return row + + mock_fetcher = MagicMock() + mock_fetcher.close = MagicMock() + + with ( + patch.object(mod, "CrawlFrontier", _FakeFrontier), + patch.object(mod, "build_fetcher", return_value=mock_fetcher), + patch.object(mod.Crawler, "worker", side_effect=_worker), + ): + crawler = mod.Crawler("https://example.com", max_pages=10) + df = crawler.crawl(show_progress=False) + + mod._PAUSE_EVENT.clear() + + assert crawler.paused is True + urls = set(df["url"].tolist()) + assert "https://example.com/fast" in urls + assert "https://example.com/slow" in urls # dropped before the drain fix + + def test_pause_loop_os_unlink_error_is_swallowed(tmp_path, monkeypatch): """OSError from os.unlink during pause-file cleanup is silently swallowed.""" import website_profiling.crawl.crawler as mod diff --git a/tests/test_db_store_coverage_gaps.py b/tests/test_db_store_coverage_gaps.py index e2ee573f..20136761 100644 --- a/tests/test_db_store_coverage_gaps.py +++ b/tests/test_db_store_coverage_gaps.py @@ -192,6 +192,40 @@ def test_issue_status_store() -> None: upsert_issue_status(conn3, property_id=1, message="x", status="open") +def test_saved_filter_store() -> None: + from website_profiling.db.saved_filter_store import ( + delete_saved_filter, + list_saved_filters, + upsert_saved_filter, + ) + + row = { + "id": 1, + "property_id": 2, + "name": "status-200", + "filter_json": {"status": ["200"]}, + "created_at": _dt(), + } + + conn = FakeConn() + conn.set_next_cursor(FakeCursor(fetchall_value=[row])) + listed = list_saved_filters(conn, 2) + assert listed[0]["name"] == "status-200" + assert listed[0]["filterJson"] == {"status": ["200"]} + + conn2 = FakeConn() + upsert_saved_filter(conn2, 2, "status-200", {"status": ["301"]}) + assert conn2.commits == 1 + + conn3 = FakeConn() + conn3.set_next_cursor(FakeCursor(rowcount=1)) + assert delete_saved_filter(conn3, 2, "status-200") is True + + conn4 = FakeConn() + conn4.set_next_cursor(FakeCursor(rowcount=0)) + assert delete_saved_filter(conn4, 2, "missing") is False + + def test_content_draft_store_paths() -> None: from website_profiling.db.content_draft_store import ( create_content_draft, diff --git a/tests/test_db_stores_unit.py b/tests/test_db_stores_unit.py index ef63db41..94c9280e 100644 --- a/tests/test_db_stores_unit.py +++ b/tests/test_db_stores_unit.py @@ -255,3 +255,49 @@ def execute(self, sql, params=None): write_report_payload(conn, {"site_name": "Y", "categories": []}) # type: ignore[arg-type] assert conn.commits == 1 + +def test_read_report_payloads_batch_empty_and_rows() -> None: + from website_profiling.db.report_store import read_report_payloads, read_report_payloads_portfolio + + assert read_report_payloads(FakeConn(), []) == {} # type: ignore[arg-type] + assert read_report_payloads_portfolio(FakeConn(), []) == {} # type: ignore[arg-type] + + conn = FakeConn() + conn.set_next_cursor( + FakeCursor( + fetchall_value=[ + {"id": 1, "data": {"site_name": "A", "summary": {"urls": 10}}}, + {"id": 2, "data": '{"site_name":"B"}'}, + {"id": None, "data": {"skip": True}}, + {"id": 3, "data": "not-json"}, + ] + ) + ) + assert read_report_payloads(conn, [1, 2, 3]) == { # type: ignore[arg-type] + 1: {"site_name": "A", "summary": {"urls": 10}}, + 2: {"site_name": "B"}, + } + + conn = FakeConn() + conn.set_next_cursor( + FakeCursor( + fetchall_value=[ + {"id": 5, "data": {"site_name": "Lite", "summary": {"score": 80}}}, + {"id": None, "data": {"skip": True}}, + ] + ) + ) + assert read_report_payloads_portfolio(conn, [5]) == {5: {"site_name": "Lite", "summary": {"score": 80}}} # type: ignore[arg-type] + + +def test_read_report_payloads_batch_execute_failure() -> None: + from website_profiling.db.report_store import read_report_payloads, read_report_payloads_portfolio + + class _BoomConn(FakeConn): + def execute(self, sql, params=None): + raise RuntimeError("db down") + + boom = _BoomConn() + assert read_report_payloads(boom, [1]) == {} # type: ignore[arg-type] + assert read_report_payloads_portfolio(boom, [1]) == {} # type: ignore[arg-type] + diff --git a/tests/tools/test_geo_parity.py b/tests/tools/test_geo_parity.py index 44969d36..50d8f461 100644 --- a/tests/tools/test_geo_parity.py +++ b/tests/tools/test_geo_parity.py @@ -716,24 +716,3 @@ def test_tool_handlers_new_tools_registered() -> None: for tool in new_tools: assert tool in _TOOL_HANDLERS, f"Tool '{tool}' not in _TOOL_HANDLERS" - -# --------------------------------------------------------------------------- -# Wiring: auditToolAllowlist -# --------------------------------------------------------------------------- - -def test_allowlist_new_tools(): - """Snapshot test: new GEO tools must be in the TS allowlist source.""" - import pathlib - source = pathlib.Path(__file__).parents[2] / "web" / "src" / "server" / "auditToolAllowlist.ts" - text = source.read_text() - new_tools = [ - "get_ai_discovery_status", - "get_robots_ai_access_score", - "get_citability_score", - "generate_schema", - "generate_geo_fix_bundle", - "check_ai_citations_live", - "compare_geo_score_deltas", - ] - for tool in new_tools: - assert f"'{tool}'" in text, f"'{tool}' missing from auditToolAllowlist.ts" diff --git a/tests/tools/test_sql_query_tool.py b/tests/tools/test_sql_query_tool.py index 057a12f7..36e265f3 100644 --- a/tests/tools/test_sql_query_tool.py +++ b/tests/tools/test_sql_query_tool.py @@ -260,6 +260,12 @@ def test_content_drafts(self) -> None: with pytest.raises(ReadOnlyViolation): assert_read_only("SELECT * FROM content_drafts") + def test_properties_rejected(self) -> None: + # properties holds google_refresh_token / OAuth creds — must not be + # reachable from the chat SQL tool. + with pytest.raises(ReadOnlyViolation): + assert_read_only("SELECT google_refresh_token FROM properties") + # --------------------------------------------------------------------------- # assert_read_only — rejected: table allowlist (non-secret unlisted tables) diff --git a/tests/tools/test_tools_gate_remaining_coverage.py b/tests/tools/test_tools_gate_remaining_coverage.py index 4e8f30fa..d2461499 100644 --- a/tests/tools/test_tools_gate_remaining_coverage.py +++ b/tests/tools/test_tools_gate_remaining_coverage.py @@ -429,6 +429,16 @@ def test_geo_tools_depth_and_fetch_helpers() -> None: assert disc_err["endpoints"] +def test_score_robots_ai_access_handles_request_error() -> None: + """robots.txt fetch failure → unchecked result (covers the RequestException branch).""" + with patch( + "website_profiling.tools.audit_tools.geo.geo_tools.requests.get", + side_effect=requests.RequestException("boom"), + ): + result = geo_mod._score_robots_ai_access("ex.com") + assert result == {"robots_score": 0, "checked": False, "error": "robots.txt not reachable"} + + # --------------------------------------------------------------------------- # integration_tools # --------------------------------------------------------------------------- diff --git a/web/.dockerignore b/web/.dockerignore new file mode 100644 index 00000000..5edb67d8 --- /dev/null +++ b/web/.dockerignore @@ -0,0 +1,4 @@ +node_modules +dist +.env.local +*.log diff --git a/web/.gitignore b/web/.gitignore index 5ef6a520..f36d0964 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -13,11 +13,12 @@ # testing /coverage -# next.js +# Vite build output +/dist + +# legacy Next.js artifacts (safe to ignore if present) /.next/ /out/ - -# production /build # misc diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 00000000..46d491f0 --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,26 @@ +# Vite SPA — static assets served by nginx. +# Build: docker build --build-arg VITE_BFF_BASE_URL=http://localhost:8090 -t website-profiling-web . + +FROM node:20-bookworm-slim AS build + +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci + +COPY . . + +ARG VITE_BFF_BASE_URL=http://localhost:8090 +ARG VITE_BASE_PATH= +ENV VITE_BFF_BASE_URL=$VITE_BFF_BASE_URL +ENV VITE_BASE_PATH=$VITE_BASE_PATH + +RUN npm run build + +FROM nginx:1.27-alpine + +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 80 + +HEALTHCHECK CMD wget -qO- http://127.0.0.1/home | grep -q '
' || exit 1 diff --git a/web/README.md b/web/README.md index 0cc2f8e5..712a2999 100644 --- a/web/README.md +++ b/web/README.md @@ -1,25 +1,28 @@ # Site Audit — Web UI -Next.js frontend for [Site Audit](../README.md). The app reads audit data from PostgreSQL and spawns the Python pipeline for crawl and report jobs. +Vite + React SPA for [Site Audit](../README.md). The browser talks to the .NET **BFF** (`services/Bff/`) for all `/api/*` calls; the BFF proxies to FastAPI and FileService. ## Development -Use the repo root scripts — do not run `npm run dev` in isolation unless Postgres is already up: +Use the repo root scripts — do not run `npm run dev` in isolation unless Postgres, FastAPI, and the BFF are already up: ```bash ./local-run setup # first time -./local-run # http://localhost:3000/home +./local-run # Vite on http://localhost:3000, BFF on :8090 ``` +Env (optional): copy `web/.env.example` to `web/.env.local` and set `VITE_BFF_BASE_URL`. + ## Structure | Path | Purpose | |------|---------| -| `app/` | App Router pages and `/api` route handlers | -| `src/components/` | Shared React components | +| `index.html` | HTML shell (theme bootstrap script) | +| `src/main.tsx` | Vite entry — `BrowserRouter` + providers | +| `src/AppRoutes.tsx` | React Router route table | | `src/views/` | Report views (overview, issues, links, …) | -| `src/server/` | DB access, pipeline jobs, config I/O | -| `src/lib/` | Schemas (`pipelineConfigSchema.ts`, `llmConfigSchema.ts`) | +| `src/components/` | Shared React components | +| `src/lib/publicBase.ts` | BFF base URL + `apiFetch` / `apiUrl` | | `public/` | Static assets (logo, favicon) | ## Commands @@ -27,15 +30,20 @@ Use the repo root scripts — do not run `npm run dev` in isolation unless Postg Run from `web/`: ```bash +npm run dev # Vite dev server (:3000) +npm run build # Production build → dist/ +npm run preview # Serve dist/ locally npm run typecheck npm run lint npm test ``` +Production image: `web/Dockerfile` (build → nginx serving `dist/`). + Full CI parity from repo root: `./local-test web`. ## Further reading - [README.md](../README.md) — setup and configuration -- [AGENT.md](../AGENT.md) — API routes, React footguns, where to edit +- [AGENT.md](../AGENT.md) — API surface (BFF), React footguns, where to edit - [docs/GLOSSARY.md](../docs/GLOSSARY.md) — UI terminology (`web/src/strings.json`) diff --git a/web/app/(reports)/[slug]/page.tsx b/web/app/(reports)/[slug]/page.tsx deleted file mode 100644 index 71b3d8f1..00000000 --- a/web/app/(reports)/[slug]/page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import ReportShell from '@/ReportShell'; -import { pathSlugToViewId } from '@/routes'; -import { strings } from '@/lib/strings'; -import { notFound } from 'next/navigation'; -import type { Metadata } from 'next'; -import type { ReactElement } from 'react'; - -export const dynamic = 'force-dynamic'; - -export async function generateMetadata({ - params, -}: { - params: Promise<{ slug: string }>; -}): Promise { - const { slug } = await params; - const viewId = pathSlugToViewId(slug); - if (!viewId) { - return { title: 'Not found' }; - } - const navEntry = strings.nav[viewId as keyof typeof strings.nav]; - const label = - navEntry && typeof navEntry === 'object' && 'label' in navEntry - ? String(navEntry.label) - : 'Report'; - return { title: `${label} · Site Audit` }; -} - -export default async function SlugPage({ - params, -}: { - params: Promise<{ slug: string }>; -}): Promise { - const { slug } = await params; - if (!pathSlugToViewId(slug)) { - notFound(); - } - return ; -} diff --git a/web/app/(reports)/error.tsx b/web/app/(reports)/error.tsx deleted file mode 100644 index e6f7f745..00000000 --- a/web/app/(reports)/error.tsx +++ /dev/null @@ -1,38 +0,0 @@ -'use client'; - -import Link from 'next/link'; -import { strings } from '@/lib/strings'; - -export default function ReportsError({ - error, - reset, -}: { - error: Error & { digest?: string }; - reset: () => void; -}) { - return ( -
-
-

{strings.app.failedTitle}

-

- {error.message || strings.app.failedHint} -

-
- - - Go to Home - -
-
-
- ); -} diff --git a/web/app/(reports)/layout.tsx b/web/app/(reports)/layout.tsx deleted file mode 100644 index bb8af3ea..00000000 --- a/web/app/(reports)/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Suspense, type ReactNode } from 'react'; -import { ReportAppClient } from '@/ReportShell'; -import ReportShellSkeleton from '@/components/ReportShellSkeleton'; - -export default function ReportsLayout({ children }: { children: ReactNode }) { - return ( - }> - {children} - - ); -} diff --git a/web/app/(reports)/loading.tsx b/web/app/(reports)/loading.tsx deleted file mode 100644 index 2468452c..00000000 --- a/web/app/(reports)/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import ReportShellSkeleton from '@/components/ReportShellSkeleton'; - -export default function ReportsLoading() { - return ; -} diff --git a/web/app/api/ai/fix-suggestion/route.ts b/web/app/api/ai/fix-suggestion/route.ts deleted file mode 100644 index 4d5714f3..00000000 --- a/web/app/api/ai/fix-suggestion/route.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); if (denied) return denied; return proxyToFastAPI(request, '/api/ai/fix-suggestion'); -}; diff --git a/web/app/api/alerts/check/route.ts b/web/app/api/alerts/check/route.ts deleted file mode 100644 index 1d765baa..00000000 --- a/web/app/api/alerts/check/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/alerts/check'); -}; diff --git a/web/app/api/app-settings/route.ts b/web/app/api/app-settings/route.ts deleted file mode 100644 index ec9a44a1..00000000 --- a/web/app/api/app-settings/route.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/app-settings'); -}; - -export const PUT: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/app-settings'); -}; diff --git a/web/app/api/auth/login/route.ts b/web/app/api/auth/login/route.ts deleted file mode 100644 index 024a2b39..00000000 --- a/web/app/api/auth/login/route.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { NextResponse, type NextRequest } from 'next/server'; -import { - authEnabled, - createSessionToken, - defaultSessionRole, - parseBasicAuth, -} from '@/server/auth'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const runtime = 'nodejs'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - if (!authEnabled()) { - return NextResponse.json({ ok: true, auth: 'disabled' }); - } - if (!parseBasicAuth(request)) { - return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 }); - } - const token = createSessionToken(defaultSessionRole()); - const res = NextResponse.json({ ok: true }); - res.cookies.set('wp_session', token, { - httpOnly: true, - sameSite: 'lax', - path: '/', - maxAge: 60 * 60 * 24 * 7, - }); - return res; -}; diff --git a/web/app/api/auth/session/route.ts b/web/app/api/auth/session/route.ts deleted file mode 100644 index ed7a1379..00000000 --- a/web/app/api/auth/session/route.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NextResponse, type NextRequest } from 'next/server'; -import { - authEnabled, - canMutateRole, - sessionRoleFromRequest, -} from '@/server/auth'; -import type { ApiRouteHandler } from '@/types/api'; - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; - -/** GET /api/auth/session — current role and mutation permissions for UI guards. */ -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const enabled = authEnabled(); - const role = sessionRoleFromRequest(request); - return NextResponse.json({ - authEnabled: enabled, - authenticated: !enabled || Boolean(role), - role: role ?? (enabled ? null : 'analyst'), - canMutate: canMutateRole(role ?? (enabled ? null : 'analyst')), - readonly: enabled && Boolean(role) && !canMutateRole(role), - }); -}; diff --git a/web/app/api/backlinks/competitor-import/route.ts b/web/app/api/backlinks/competitor-import/route.ts deleted file mode 100644 index 7e223d22..00000000 --- a/web/app/api/backlinks/competitor-import/route.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { requireApiAuth } from '@/server/auth'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuth(request); - if (authDenied) return authDenied; - return proxyToFastAPI(request, '/api/backlinks/competitor-import'); -}; diff --git a/web/app/api/backlinks/third-party-import/route.ts b/web/app/api/backlinks/third-party-import/route.ts deleted file mode 100644 index e93189a8..00000000 --- a/web/app/api/backlinks/third-party-import/route.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { requireApiAuth } from '@/server/auth'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuth(request); - if (authDenied) return authDenied; - return proxyToFastAPI(request, '/api/backlinks/third-party-import'); -}; diff --git a/web/app/api/backlinks/velocity/route.ts b/web/app/api/backlinks/velocity/route.ts deleted file mode 100644 index b2a13048..00000000 --- a/web/app/api/backlinks/velocity/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - return proxyToFastAPI(request, '/api/backlinks/velocity'); -}; diff --git a/web/app/api/chat/artifacts/[id]/route.ts b/web/app/api/chat/artifacts/[id]/route.ts deleted file mode 100644 index 4933b267..00000000 --- a/web/app/api/chat/artifacts/[id]/route.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * GET /api/chat/artifacts/[id] — retrieve an AI-generated artifact file via FastAPI. - */ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { requireApiAuthForChat } from '@/server/auth'; -import type { ApiRouteHandlerWithParams } from '@/types/api'; - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandlerWithParams<{ id: string }> = async ( - request: NextRequest, - context: { params: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuthForChat(request); - if (authDenied) return authDenied; - const { id } = await context.params; - return proxyToFastAPI(request, `/api/chat/artifacts/${id}`); -}; diff --git a/web/app/api/chat/route.ts b/web/app/api/chat/route.ts deleted file mode 100644 index ded08f56..00000000 --- a/web/app/api/chat/route.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * POST /api/chat — stream agent response via FastAPI SSE. - * FastAPI runs the Python agent directly and streams text/event-stream. - */ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { requireApiAuthForChat } from '@/server/auth'; -import type { ApiRouteHandler } from '@/types/api'; - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuthForChat(request); - if (authDenied) return authDenied; - return proxyToFastAPI(request, '/api/chat/'); -}; diff --git a/web/app/api/chat/sessions/[id]/messages/route.ts b/web/app/api/chat/sessions/[id]/messages/route.ts deleted file mode 100644 index c2b2ebb8..00000000 --- a/web/app/api/chat/sessions/[id]/messages/route.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * GET /api/chat/sessions/[id]/messages — get chat session messages via FastAPI. - */ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { requireApiAuthForChat } from '@/server/auth'; -import type { ApiRouteHandler } from '@/types/api'; - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async ( - request: NextRequest, - context?: { params?: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuthForChat(request); - if (authDenied) return authDenied; - const params = context?.params ? await context.params : { id: '' }; - return proxyToFastAPI(request, `/api/chat/sessions/${params.id}/messages`); -}; diff --git a/web/app/api/chat/sessions/[id]/route.ts b/web/app/api/chat/sessions/[id]/route.ts deleted file mode 100644 index 330116f3..00000000 --- a/web/app/api/chat/sessions/[id]/route.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * GET/DELETE /api/chat/sessions/[id] — get or delete a chat session via FastAPI. - */ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { requireApiAuth, requireApiAuthForChat } from '@/server/auth'; -import type { ApiRouteHandler } from '@/types/api'; - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async ( - request: NextRequest, - context?: { params?: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuthForChat(request); - if (authDenied) return authDenied; - const params = context?.params ? await context.params : { id: '' }; - return proxyToFastAPI(request, `/api/chat/sessions/${params.id}`); -}; - -export const DELETE: ApiRouteHandler = async ( - request: NextRequest, - context?: { params?: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuth(request); - if (authDenied) return authDenied; - const params = context?.params ? await context.params : { id: '' }; - return proxyToFastAPI(request, `/api/chat/sessions/${params.id}`); -}; diff --git a/web/app/api/chat/sessions/route.ts b/web/app/api/chat/sessions/route.ts deleted file mode 100644 index badf4e24..00000000 --- a/web/app/api/chat/sessions/route.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * GET/POST /api/chat/sessions — list or create chat sessions via FastAPI. - */ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { requireApiAuthForChat } from '@/server/auth'; -import type { ApiRouteHandler } from '@/types/api'; - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuthForChat(request); - if (authDenied) return authDenied; - return proxyToFastAPI(request, '/api/chat/sessions'); -}; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuthForChat(request); - if (authDenied) return authDenied; - return proxyToFastAPI(request, '/api/chat/sessions'); -}; diff --git a/web/app/api/compare/export/route.ts b/web/app/api/compare/export/route.ts deleted file mode 100644 index 35da28ee..00000000 --- a/web/app/api/compare/export/route.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); if (denied) return denied; return proxyToFastAPI(request, '/api/compare/export'); -}; diff --git a/web/app/api/content-drafts/[id]/route.ts b/web/app/api/content-drafts/[id]/route.ts deleted file mode 100644 index d5ae755f..00000000 --- a/web/app/api/content-drafts/[id]/route.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { requireApiAuth } from '@/server/auth'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async ( - request: NextRequest, - context?: { params?: Promise<{ id: string }> }, -): Promise => { - const params = context?.params ? await context.params : { id: '' }; - return proxyToFastAPI(request, `/api/content-drafts/${params.id}`); -}; - -export const PATCH: ApiRouteHandler = async ( - request: NextRequest, - context?: { params?: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuth(request); - if (authDenied) return authDenied; - const params = context?.params ? await context.params : { id: '' }; - return proxyToFastAPI(request, `/api/content-drafts/${params.id}`); -}; - -export const DELETE: ApiRouteHandler = async ( - request: NextRequest, - context?: { params?: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuth(request); - if (authDenied) return authDenied; - const params = context?.params ? await context.params : { id: '' }; - return proxyToFastAPI(request, `/api/content-drafts/${params.id}`); -}; diff --git a/web/app/api/content-drafts/route.ts b/web/app/api/content-drafts/route.ts deleted file mode 100644 index e860a4a6..00000000 --- a/web/app/api/content-drafts/route.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { requireApiAuth } from '@/server/auth'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - return proxyToFastAPI(request, '/api/content-drafts'); -}; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuth(request); - if (authDenied) return authDenied; - return proxyToFastAPI(request, '/api/content-drafts'); -}; diff --git a/web/app/api/content/analyze/route.ts b/web/app/api/content/analyze/route.ts deleted file mode 100644 index 02ef08ed..00000000 --- a/web/app/api/content/analyze/route.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { requireApiAuth } from '@/server/auth'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuth(request); - if (authDenied) return authDenied; - return proxyToFastAPI(request, '/api/content/analyze'); -}; diff --git a/web/app/api/content/score/route.ts b/web/app/api/content/score/route.ts deleted file mode 100644 index 28a34e13..00000000 --- a/web/app/api/content/score/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - return proxyToFastAPI(request, '/api/content/score'); -}; diff --git a/web/app/api/content/wizard/route.ts b/web/app/api/content/wizard/route.ts deleted file mode 100644 index 5fad8841..00000000 --- a/web/app/api/content/wizard/route.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { requireApiAuth } from '@/server/auth'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuth(request); - if (authDenied) return authDenied; - return proxyToFastAPI(request, '/api/content/wizard'); -}; diff --git a/web/app/api/crawl/browser-status/route.ts b/web/app/api/crawl/browser-status/route.ts deleted file mode 100644 index 75864cf3..00000000 --- a/web/app/api/crawl/browser-status/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/crawl/browser-status'); -}; diff --git a/web/app/api/crawl/page-html/route.ts b/web/app/api/crawl/page-html/route.ts deleted file mode 100644 index 095defde..00000000 --- a/web/app/api/crawl/page-html/route.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/crawl/page-html'); -}; - -export const DELETE: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/crawl/page-html'); -}; diff --git a/web/app/api/dashboards/[id]/route.ts b/web/app/api/dashboards/[id]/route.ts deleted file mode 100644 index 6e980bc0..00000000 --- a/web/app/api/dashboards/[id]/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandlerWithParams } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandlerWithParams<{ id: string }> = async ( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const { id } = await params; - return proxyToFastAPI(request, `/api/dashboards/${id}`); -}; - -export const PUT: ApiRouteHandlerWithParams<{ id: string }> = async ( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const { id } = await params; - return proxyToFastAPI(request, `/api/dashboards/${id}`); -}; - -export const DELETE: ApiRouteHandlerWithParams<{ id: string }> = async ( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const { id } = await params; - return proxyToFastAPI(request, `/api/dashboards/${id}`); -}; diff --git a/web/app/api/dashboards/ai-generate/route.ts b/web/app/api/dashboards/ai-generate/route.ts deleted file mode 100644 index 7663f346..00000000 --- a/web/app/api/dashboards/ai-generate/route.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { NextResponse, type NextRequest } from 'next/server'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { fastApiBase } from '@/server/fastApiClient'; -import { DASHBOARD_CATALOG, dimensions, measures } from '@/lib/dashboard/catalog/catalog'; -import { VIZ_LABELS } from '@/lib/dashboard/viz/labels'; -import type { ApiRouteHandler } from '@/types/api'; - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; - -const DASHSCRIPT_HELP = ` -DashScript is a lightweight formula language for dashboard widgets. - -MEASURE (scalar formula, produces a single number or string): - field("key") — value from root result by dot-path key - sum("col") — sum of numeric column across all rows - avg("col") — average - count() — number of rows - min("col") / max("col") — min / max of column - if(cond, thenVal, elseVal) — conditional - coalesce(a, b, c) — first non-null value - Arithmetic: + - * / (division by zero returns null) - Comparison: == != < <= > >= - Logical: && || ! - -TRANSFORM (row pipeline, applied to rows array before rendering): - filter(expr) — keep rows where expr is truthy (use row column names directly) - sort(col, asc|desc) — sort rows by column (default asc) - take(N) — keep first N rows - skip(N) — drop first N rows - project(col1, col2) — keep only listed columns - Stages are joined with | e.g. filter(count > 0) | sort(count, desc) | take(10) - -Examples: - measure: field("health_score") - measure: sum("issues") / count() - transform: filter(severity == "critical") | sort(count, desc) | take(5) -`.trim(); - -/** - * POST /api/dashboards/ai-generate - * Body: { mode, prompt, toolName?, propertyId?, reportId?, current? } - */ -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - - let body: { - mode?: string; - prompt?: string; - toolName?: string; - propertyId?: number; - reportId?: number | null; - current?: unknown; - }; - try { - body = await request.json(); - } catch { - return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); - } - - const mode = String(body.mode || 'widget').trim().toLowerCase(); - if (!['script', 'widget', 'dashboard'].includes(mode)) { - return NextResponse.json({ error: 'mode must be script, widget, or dashboard' }, { status: 400 }); - } - const prompt = String(body.prompt || '').trim(); - if (!prompt) { - return NextResponse.json({ error: 'prompt required' }, { status: 400 }); - } - - const payload = { - mode, - prompt, - toolName: String(body.toolName || '').trim() || undefined, - propertyId: Number(body.propertyId || 0) || undefined, - reportId: body.reportId != null ? Number(body.reportId) : undefined, - catalog: DASHBOARD_CATALOG.map((e) => ({ - toolName: e.toolName, - label: e.label, - section: e.section, - fields: e.fields, - dimensions: dimensions(e).map((f) => ({ - key: f.key, - label: f.label, - defaultAgg: f.defaultAgg, - format: f.format, - })), - measures: measures(e).map((f) => ({ - key: f.key, - label: f.label, - defaultAgg: f.defaultAgg, - format: f.format, - })), - rowsPath: e.rowsPath, - compatibleViz: e.compatibleViz, - })), - viz_types: VIZ_LABELS, - dashscript_help: DASHSCRIPT_HELP, - current: body.current ?? null, - }; - - try { - const res = await fetch(`${fastApiBase()}/api/dashboards/ai-generate`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - cache: 'no-store', - }); - const data = (await res.json().catch(() => ({}))) as Record; - return NextResponse.json(data, { status: res.status }); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - return NextResponse.json({ error: msg || 'AI generation failed' }, { status: 500 }); - } -}; diff --git a/web/app/api/dashboards/route.ts b/web/app/api/dashboards/route.ts deleted file mode 100644 index 6cdc3f55..00000000 --- a/web/app/api/dashboards/route.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/dashboards'); -}; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/dashboards'); -}; diff --git a/web/app/api/filters/route.ts b/web/app/api/filters/route.ts deleted file mode 100644 index 6ca3b5de..00000000 --- a/web/app/api/filters/route.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/filters'); -}; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/filters'); -}; - -export const DELETE: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/filters'); -}; diff --git a/web/app/api/health/route.ts b/web/app/api/health/route.ts deleted file mode 100644 index cb9f5a41..00000000 --- a/web/app/api/health/route.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import type { ApiRouteHandler } from '@/types/api'; - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - return proxyToFastAPI(request, '/api/health'); -}; diff --git a/web/app/api/integrations/bing/sync/route.ts b/web/app/api/integrations/bing/sync/route.ts deleted file mode 100644 index 710b9149..00000000 --- a/web/app/api/integrations/bing/sync/route.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); if (denied) return denied; return proxyToFastAPI(request, '/api/integrations/bing/sync'); -}; diff --git a/web/app/api/integrations/google/auth/route.ts b/web/app/api/integrations/google/auth/route.ts deleted file mode 100644 index 7b323d15..00000000 --- a/web/app/api/integrations/google/auth/route.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { NextResponse, type NextRequest } from 'next/server'; -import { randomUUID } from 'crypto'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { - getClientIdForOAuth, - loadGoogleAppSettings, -} from '@/server/googleAppSettings'; -import { getPropertyById, resolvePropertyIdFromStartUrl } from '@/server/propertiesDb'; -import { - GOOGLE_OAUTH_RETURN_COOKIE, - validateOAuthReturnPath, -} from '@/server/oauthReturn'; -import type { ApiRouteHandler } from '@/types/api'; - -export const runtime = 'nodejs'; - -const SCOPES = [ - 'https://www.googleapis.com/auth/webmasters.readonly', - 'https://www.googleapis.com/auth/analytics.readonly', - 'https://www.googleapis.com/auth/adwords', -].join(' '); - -const OAUTH_COOKIE_OPTS = { - httpOnly: true, - maxAge: 300, - path: '/', - sameSite: 'lax' as const, -}; - -const OAUTH_PROPERTY_COOKIE = 'google_oauth_property_id'; - -/** - * GET /api/integrations/google/auth - * Generates a CSRF state token, stores return path, redirects to Google OAuth. - */ -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - - let propertyIdRaw = request.nextUrl.searchParams.get('propertyId'); - if (!propertyIdRaw) { - const startUrl = request.nextUrl.searchParams.get('startUrl')?.trim() || ''; - if (startUrl) { - try { - const id = await resolvePropertyIdFromStartUrl(startUrl); - if (id != null) propertyIdRaw = String(id); - } catch { - /* fall through */ - } - } - } - if (!propertyIdRaw) { - return NextResponse.json( - { error: 'propertyId is required. Set Site URL and connect from Integrations.' }, - { status: 400 }, - ); - } - - const appRow = await loadGoogleAppSettings(); - const clientId = getClientIdForOAuth(appRow); - const redirectUri = - process.env.GOOGLE_REDIRECT_URI || - `http://localhost:3000/api/integrations/google/callback`; - - if (!clientId) { - return NextResponse.json( - { - error: - 'No Google Client ID configured. Go to Integrations and complete Step 1 first.', - }, - { status: 400 }, - ); - } - - const state = randomUUID(); - const returnTo = validateOAuthReturnPath(request.nextUrl.searchParams.get('returnTo')); - - const pid = parseInt(propertyIdRaw, 10); - if (!Number.isFinite(pid)) { - return NextResponse.json({ error: 'Invalid propertyId' }, { status: 400 }); - } - const row = await getPropertyById(pid); - if (!row) { - return NextResponse.json({ error: 'Property not found' }, { status: 404 }); - } - const propertyId = pid; - - const params = new URLSearchParams({ - client_id: clientId, - redirect_uri: redirectUri, - response_type: 'code', - scope: SCOPES, - access_type: 'offline', - prompt: 'consent', - state, - }); - - const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`; - - const response = NextResponse.redirect(authUrl); - response.cookies.set('google_oauth_state', state, OAUTH_COOKIE_OPTS); - response.cookies.set(GOOGLE_OAUTH_RETURN_COOKIE, returnTo, OAUTH_COOKIE_OPTS); - response.cookies.set(OAUTH_PROPERTY_COOKIE, String(propertyId), OAUTH_COOKIE_OPTS); - return response; -}; diff --git a/web/app/api/integrations/google/callback/route.ts b/web/app/api/integrations/google/callback/route.ts deleted file mode 100644 index fc19d863..00000000 --- a/web/app/api/integrations/google/callback/route.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { NextResponse, type NextRequest } from 'next/server'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { - getClientIdForOAuth, - getClientSecretForOAuth, - loadGoogleAppSettings, -} from '@/server/googleAppSettings'; -import { setPropertyGoogleCredentials } from '@/server/propertiesDb'; -import { - GOOGLE_OAUTH_RETURN_COOKIE, - oauthRedirectUrl, - validateOAuthReturnPath, -} from '@/server/oauthReturn'; -import type { ApiRouteHandler } from '@/types/api'; - -export const runtime = 'nodejs'; - -interface TokenExchangeResponse { - refresh_token?: string; - error?: string; - error_description?: string; -} - -function appBaseFromEnv(): string { - return process.env.GOOGLE_REDIRECT_URI - ? process.env.GOOGLE_REDIRECT_URI.replace('/api/integrations/google/callback', '') - : 'http://localhost:3000'; -} - -function oauthErrorRedirect( - appBase: string, - returnPath: string, - reason: string, -): NextResponse { - return NextResponse.redirect( - oauthRedirectUrl(appBase, returnPath, { - integrations: 'open', - auth: 'error', - reason, - }), - ); -} - -const OAUTH_PROPERTY_COOKIE = 'google_oauth_property_id'; - -function clearOAuthCookies(response: NextResponse): void { - response.cookies.set('google_oauth_state', '', { maxAge: 0, path: '/' }); - response.cookies.set(GOOGLE_OAUTH_RETURN_COOKIE, '', { maxAge: 0, path: '/' }); - response.cookies.set(OAUTH_PROPERTY_COOKIE, '', { maxAge: 0, path: '/' }); -} - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - - const { searchParams } = request.nextUrl; - const code = searchParams.get('code'); - const state = searchParams.get('state'); - const errorParam = searchParams.get('error'); - - const appBase = appBaseFromEnv(); - const returnPath = validateOAuthReturnPath( - request.cookies.get(GOOGLE_OAUTH_RETURN_COOKIE)?.value, - ); - - if (errorParam) { - const response = oauthErrorRedirect(appBase, returnPath, errorParam); - clearOAuthCookies(response); - return response; - } - - const cookieState = request.cookies.get('google_oauth_state')?.value; - if (!state || !cookieState || state !== cookieState) { - const response = oauthErrorRedirect( - appBase, - returnPath, - 'Invalid state parameter. Please try connecting again.', - ); - clearOAuthCookies(response); - return response; - } - - if (!code) { - const response = oauthErrorRedirect( - appBase, - returnPath, - 'No authorization code received.', - ); - clearOAuthCookies(response); - return response; - } - - const propertyCookie = request.cookies.get(OAUTH_PROPERTY_COOKIE)?.value; - const propertyId = propertyCookie ? parseInt(propertyCookie, 10) : NaN; - if (!Number.isFinite(propertyId) || propertyId <= 0) { - const response = oauthErrorRedirect( - appBase, - returnPath, - 'Missing property context. Set Site URL and connect Google from Integrations again.', - ); - clearOAuthCookies(response); - return response; - } - - const appRow = await loadGoogleAppSettings(); - const clientId = getClientIdForOAuth(appRow); - const clientSecret = getClientSecretForOAuth(appRow); - const redirectUri = - process.env.GOOGLE_REDIRECT_URI || - `http://localhost:3000/api/integrations/google/callback`; - - if (!clientId || !clientSecret) { - const response = oauthErrorRedirect( - appBase, - returnPath, - 'Client credentials missing. Complete Step 1 in Integrations.', - ); - clearOAuthCookies(response); - return response; - } - - try { - const tokenRes = await fetch('https://oauth2.googleapis.com/token', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ - code, - client_id: clientId, - client_secret: clientSecret, - redirect_uri: redirectUri, - grant_type: 'authorization_code', - }).toString(), - }); - - const tokenData = (await tokenRes.json()) as TokenExchangeResponse; - if (!tokenRes.ok || !tokenData.refresh_token) { - const reason = tokenData.error_description || tokenData.error || 'Token exchange failed'; - const response = oauthErrorRedirect(appBase, returnPath, reason); - clearOAuthCookies(response); - return response; - } - - await setPropertyGoogleCredentials(propertyId, { - refreshToken: tokenData.refresh_token, - authMode: 'oauth', - }); - - const response = NextResponse.redirect( - oauthRedirectUrl(appBase, returnPath, { - integrations: 'open', - auth: 'success', - }), - ); - clearOAuthCookies(response); - return response; - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - const response = oauthErrorRedirect(appBase, returnPath, msg); - clearOAuthCookies(response); - return response; - } -}; diff --git a/web/app/api/integrations/google/credentials/route.ts b/web/app/api/integrations/google/credentials/route.ts deleted file mode 100644 index 44454b50..00000000 --- a/web/app/api/integrations/google/credentials/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/integrations/google/credentials'); -}; diff --git a/web/app/api/integrations/google/credentials/upload/route.ts b/web/app/api/integrations/google/credentials/upload/route.ts deleted file mode 100644 index f88000ae..00000000 --- a/web/app/api/integrations/google/credentials/upload/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/integrations/google/credentials/upload'); -}; diff --git a/web/app/api/integrations/google/disconnect/route.ts b/web/app/api/integrations/google/disconnect/route.ts deleted file mode 100644 index 6ba0472c..00000000 --- a/web/app/api/integrations/google/disconnect/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/integrations/google/disconnect'); -}; diff --git a/web/app/api/integrations/google/keywords/by-page/route.ts b/web/app/api/integrations/google/keywords/by-page/route.ts deleted file mode 100644 index e03c6531..00000000 --- a/web/app/api/integrations/google/keywords/by-page/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const guard = forbiddenIfNotLocal(request); - if (guard) return guard; - return proxyToFastAPI(request, '/api/integrations/google/keywords/by-page'); -}; diff --git a/web/app/api/integrations/google/keywords/expand/route.ts b/web/app/api/integrations/google/keywords/expand/route.ts deleted file mode 100644 index b6aaf92a..00000000 --- a/web/app/api/integrations/google/keywords/expand/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const runtime = 'nodejs'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/integrations/google/keywords/expand'); -}; diff --git a/web/app/api/integrations/google/keywords/history/batch/route.ts b/web/app/api/integrations/google/keywords/history/batch/route.ts deleted file mode 100644 index 2cd300bf..00000000 --- a/web/app/api/integrations/google/keywords/history/batch/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const runtime = 'nodejs'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/integrations/google/keywords/history/batch'); -}; diff --git a/web/app/api/integrations/google/keywords/history/route.ts b/web/app/api/integrations/google/keywords/history/route.ts deleted file mode 100644 index ad624047..00000000 --- a/web/app/api/integrations/google/keywords/history/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const guard = forbiddenIfNotLocal(request); - if (guard) return guard; - return proxyToFastAPI(request, '/api/integrations/google/keywords/history'); -}; diff --git a/web/app/api/integrations/google/keywords/planner/route.ts b/web/app/api/integrations/google/keywords/planner/route.ts deleted file mode 100644 index 3ce19392..00000000 --- a/web/app/api/integrations/google/keywords/planner/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const runtime = 'nodejs'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/integrations/google/keywords/planner'); -}; diff --git a/web/app/api/integrations/google/page-compare/route.ts b/web/app/api/integrations/google/page-compare/route.ts deleted file mode 100644 index e7fff375..00000000 --- a/web/app/api/integrations/google/page-compare/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const runtime = 'nodejs'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/integrations/google/page-compare'); -}; diff --git a/web/app/api/integrations/google/page-data/history/route.ts b/web/app/api/integrations/google/page-data/history/route.ts deleted file mode 100644 index 3c3eb648..00000000 --- a/web/app/api/integrations/google/page-data/history/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/integrations/google/page-data/history'); -}; diff --git a/web/app/api/integrations/google/page-data/route.ts b/web/app/api/integrations/google/page-data/route.ts deleted file mode 100644 index b14b4c0d..00000000 --- a/web/app/api/integrations/google/page-data/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/integrations/google/page-data'); -}; diff --git a/web/app/api/integrations/google/page-live/history/route.ts b/web/app/api/integrations/google/page-live/history/route.ts deleted file mode 100644 index 33e7b765..00000000 --- a/web/app/api/integrations/google/page-live/history/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const runtime = 'nodejs'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/integrations/google/page-live/history'); -}; diff --git a/web/app/api/integrations/google/page-live/route.ts b/web/app/api/integrations/google/page-live/route.ts deleted file mode 100644 index 59d99a32..00000000 --- a/web/app/api/integrations/google/page-live/route.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { requireApiAuth } from '@/server/auth'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuth(request); - if (authDenied) return authDenied; - return proxyToFastAPI(request, '/api/integrations/google/page-live'); -}; diff --git a/web/app/api/integrations/google/properties/route.ts b/web/app/api/integrations/google/properties/route.ts deleted file mode 100644 index e413d73b..00000000 --- a/web/app/api/integrations/google/properties/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/integrations/google/properties'); -}; diff --git a/web/app/api/integrations/google/status/route.ts b/web/app/api/integrations/google/status/route.ts deleted file mode 100644 index 84e7085f..00000000 --- a/web/app/api/integrations/google/status/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/integrations/google/status'); -}; diff --git a/web/app/api/integrations/google/test/route.ts b/web/app/api/integrations/google/test/route.ts deleted file mode 100644 index 38c7c29c..00000000 --- a/web/app/api/integrations/google/test/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/integrations/google/test'); -}; diff --git a/web/app/api/issues/action-plan/route.ts b/web/app/api/issues/action-plan/route.ts deleted file mode 100644 index b58f981a..00000000 --- a/web/app/api/issues/action-plan/route.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); if (denied) return denied; return proxyToFastAPI(request, '/api/issues/action-plan'); -}; diff --git a/web/app/api/issues/fix-suggestion/route.ts b/web/app/api/issues/fix-suggestion/route.ts deleted file mode 100644 index ea3005f0..00000000 --- a/web/app/api/issues/fix-suggestion/route.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); if (denied) return denied; return proxyToFastAPI(request, '/api/issues/fix-suggestion'); -}; diff --git a/web/app/api/issues/status/route.ts b/web/app/api/issues/status/route.ts deleted file mode 100644 index 7d101573..00000000 --- a/web/app/api/issues/status/route.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/issues/status'); -}; - -export const PUT: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/issues/status'); -}; diff --git a/web/app/api/jobs/[id]/cancel/route.ts b/web/app/api/jobs/[id]/cancel/route.ts deleted file mode 100644 index 899c4e17..00000000 --- a/web/app/api/jobs/[id]/cancel/route.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * POST /api/jobs/[id]/cancel — cancel a pipeline job via FastAPI. - */ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { requireApiAuth } from '@/server/auth'; -import type { ApiRouteHandlerWithParams } from '@/types/api'; - -export const runtime = 'nodejs'; - -export const POST: ApiRouteHandlerWithParams<{ id: string }> = async ( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuth(request); - if (authDenied) return authDenied; - const { id } = await params; - return proxyToFastAPI(request, `/api/jobs/${id}/cancel`); -}; diff --git a/web/app/api/jobs/[id]/pause/route.ts b/web/app/api/jobs/[id]/pause/route.ts deleted file mode 100644 index df855688..00000000 --- a/web/app/api/jobs/[id]/pause/route.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * POST /api/jobs/[id]/pause — pause a pipeline job via FastAPI. - */ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { requireApiAuth } from '@/server/auth'; -import type { ApiRouteHandlerWithParams } from '@/types/api'; - -export const runtime = 'nodejs'; - -export const POST: ApiRouteHandlerWithParams<{ id: string }> = async ( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuth(request); - if (authDenied) return authDenied; - const { id } = await params; - return proxyToFastAPI(request, `/api/jobs/${id}/pause`); -}; diff --git a/web/app/api/jobs/[id]/resume/route.ts b/web/app/api/jobs/[id]/resume/route.ts deleted file mode 100644 index 1f0c6ff6..00000000 --- a/web/app/api/jobs/[id]/resume/route.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * POST /api/jobs/[id]/resume — resume a paused pipeline job via FastAPI. - */ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { requireApiAuth } from '@/server/auth'; -import type { ApiRouteHandlerWithParams } from '@/types/api'; - -export const runtime = 'nodejs'; - -export const POST: ApiRouteHandlerWithParams<{ id: string }> = async ( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuth(request); - if (authDenied) return authDenied; - const { id } = await params; - return proxyToFastAPI(request, `/api/jobs/${id}/resume`); -}; diff --git a/web/app/api/jobs/[id]/route.ts b/web/app/api/jobs/[id]/route.ts deleted file mode 100644 index 999765e9..00000000 --- a/web/app/api/jobs/[id]/route.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * GET /api/jobs/[id] — get pipeline job status via FastAPI. - */ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandlerWithParams } from '@/types/api'; - -export const runtime = 'nodejs'; - -export const GET: ApiRouteHandlerWithParams<{ id: string }> = async ( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const { id } = await params; - return proxyToFastAPI(request, `/api/jobs/${id}`); -}; diff --git a/web/app/api/jobs/route.ts b/web/app/api/jobs/route.ts deleted file mode 100644 index b63460c5..00000000 --- a/web/app/api/jobs/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * GET /api/jobs — list recent pipeline jobs via FastAPI. - */ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/jobs'); -}; diff --git a/web/app/api/keywords/competitor-import/route.ts b/web/app/api/keywords/competitor-import/route.ts deleted file mode 100644 index ff78edfd..00000000 --- a/web/app/api/keywords/competitor-import/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/keywords/competitor-import'); -}; diff --git a/web/app/api/keywords/content-brief/route.ts b/web/app/api/keywords/content-brief/route.ts deleted file mode 100644 index 4d44e142..00000000 --- a/web/app/api/keywords/content-brief/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - return proxyToFastAPI(request, '/api/keywords/content-brief'); -}; diff --git a/web/app/api/links/page-coach/route.ts b/web/app/api/links/page-coach/route.ts deleted file mode 100644 index 3d91306a..00000000 --- a/web/app/api/links/page-coach/route.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { requireApiAuth } from '@/server/auth'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuth(request); - if (authDenied) return authDenied; - return proxyToFastAPI(request, '/api/links/page-coach'); -}; diff --git a/web/app/api/llm-config/route.ts b/web/app/api/llm-config/route.ts deleted file mode 100644 index ee9adbb5..00000000 --- a/web/app/api/llm-config/route.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/llm-config'); -}; - -export const PUT: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/llm-config'); -}; diff --git a/web/app/api/logs/upload/route.ts b/web/app/api/logs/upload/route.ts deleted file mode 100644 index 67f37f7f..00000000 --- a/web/app/api/logs/upload/route.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { requireApiAuth } from '@/server/auth'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuth(request); - if (authDenied) return authDenied; - return proxyToFastAPI(request, '/api/logs/upload'); -}; diff --git a/web/app/api/mcp-tools/route.ts b/web/app/api/mcp-tools/route.ts deleted file mode 100644 index 50cfcac1..00000000 --- a/web/app/api/mcp-tools/route.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { type NextRequest, NextResponse } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; - -export const dynamic = 'force-dynamic'; - -export async function GET(request: NextRequest): Promise { - const guard = forbiddenIfNotLocal(request); - if (guard) return guard; - return proxyToFastAPI(request, '/api/mcp-tools'); -} diff --git a/web/app/api/ollama/status/route.ts b/web/app/api/ollama/status/route.ts deleted file mode 100644 index 9f3ede78..00000000 --- a/web/app/api/ollama/status/route.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { NextResponse, type NextRequest } from 'next/server'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { loadLlmConfig } from '@/server/llmConfig'; -import { - fetchOllamaModels, - modelIsConfigured, - modelsSupportTools, -} from '@/server/ollamaModels'; -import type { ApiRouteHandler } from '@/types/api'; - -export const runtime = 'nodejs'; - -const DEFAULT_BASE = 'http://127.0.0.1:11434'; - -/** GET /api/ollama/status — local install + full Ollama cloud catalog. */ -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - - let baseUrl = DEFAULT_BASE; - let configuredModel = ''; - try { - const cfg = await loadLlmConfig(); - baseUrl = String(cfg.state.llm_base_url || DEFAULT_BASE).replace(/\/$/, ''); - configuredModel = String(cfg.state.llm_model || '').trim(); - } catch { - /* use defaults */ - } - - const result = await fetchOllamaModels(baseUrl); - - if (!result.ok) { - return NextResponse.json({ - ok: false, - baseUrl: result.baseUrl, - configuredModel, - error: result.error || 'Cannot reach Ollama. Is it running?', - models: [], - cloudCatalogOk: false, - localOk: false, - }); - } - - const modelInstalled = modelIsConfigured(result.models, configuredModel); - const configuredEntry = result.models.find( - (m) => m.name.toLowerCase() === configuredModel.toLowerCase(), - ); - - return NextResponse.json({ - ok: true, - baseUrl: result.baseUrl, - configuredModel, - modelInstalled, - supportsTools: - configuredEntry?.capabilities?.includes('tools') ?? modelsSupportTools(result.models), - cloudCatalogOk: result.cloudCatalogOk, - localOk: result.localOk, - catalogSource: 'live', - cloudModelCount: result.models.filter((m) => m.source === 'cloud').length, - models: result.models, - }); -}; diff --git a/web/app/api/page-markdown/content/route.ts b/web/app/api/page-markdown/content/route.ts deleted file mode 100644 index 073500e5..00000000 --- a/web/app/api/page-markdown/content/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - return proxyToFastAPI(request, '/api/page-markdown/content'); -}; diff --git a/web/app/api/page-markdown/extract/route.ts b/web/app/api/page-markdown/extract/route.ts deleted file mode 100644 index b0bf4bcb..00000000 --- a/web/app/api/page-markdown/extract/route.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { requireApiAuth } from '@/server/auth'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuth(request); - if (authDenied) return authDenied; - return proxyToFastAPI(request, '/api/page-markdown/extract'); -}; diff --git a/web/app/api/page-markdown/route.ts b/web/app/api/page-markdown/route.ts deleted file mode 100644 index 0909b613..00000000 --- a/web/app/api/page-markdown/route.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { requireApiAuth } from '@/server/auth'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - return proxyToFastAPI(request, '/api/page-markdown'); -}; - -export const DELETE: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuth(request); - if (authDenied) return authDenied; - return proxyToFastAPI(request, '/api/page-markdown'); -}; diff --git a/web/app/api/page-markdown/runs/route.ts b/web/app/api/page-markdown/runs/route.ts deleted file mode 100644 index 74876eee..00000000 --- a/web/app/api/page-markdown/runs/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - return proxyToFastAPI(request, '/api/page-markdown/runs'); -}; diff --git a/web/app/api/pipeline-config/route.ts b/web/app/api/pipeline-config/route.ts deleted file mode 100644 index 5fbb5f52..00000000 --- a/web/app/api/pipeline-config/route.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/pipeline-config'); -}; - -export const PUT: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/pipeline-config'); -}; diff --git a/web/app/api/portfolio/delete/route.ts b/web/app/api/portfolio/delete/route.ts deleted file mode 100644 index 9867ea76..00000000 --- a/web/app/api/portfolio/delete/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const DELETE: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/portfolio/delete'); -}; diff --git a/web/app/api/properties/[id]/authorize/route.ts b/web/app/api/properties/[id]/authorize/route.ts deleted file mode 100644 index 5b50dcae..00000000 --- a/web/app/api/properties/[id]/authorize/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandlerWithParams } from '@/types/api'; - -export const runtime = 'nodejs'; - -export const POST: ApiRouteHandlerWithParams<{ id: string }> = async ( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const { id } = await params; - return proxyToFastAPI(request, `/api/properties/${id}/authorize`); -}; diff --git a/web/app/api/properties/[id]/google/credentials/route.ts b/web/app/api/properties/[id]/google/credentials/route.ts deleted file mode 100644 index 6936d02d..00000000 --- a/web/app/api/properties/[id]/google/credentials/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandlerWithParams } from '@/types/api'; - -export const runtime = 'nodejs'; - -export const POST: ApiRouteHandlerWithParams<{ id: string }> = async ( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const { id } = await params; - return proxyToFastAPI(request, `/api/properties/${id}/google/credentials`); -}; diff --git a/web/app/api/properties/[id]/google/disconnect/route.ts b/web/app/api/properties/[id]/google/disconnect/route.ts deleted file mode 100644 index ce678de8..00000000 --- a/web/app/api/properties/[id]/google/disconnect/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandlerWithParams } from '@/types/api'; - -export const runtime = 'nodejs'; - -export const POST: ApiRouteHandlerWithParams<{ id: string }> = async ( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const { id } = await params; - return proxyToFastAPI(request, `/api/properties/${id}/google/disconnect`); -}; diff --git a/web/app/api/properties/[id]/google/links/import/route.ts b/web/app/api/properties/[id]/google/links/import/route.ts deleted file mode 100644 index d0327a28..00000000 --- a/web/app/api/properties/[id]/google/links/import/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandlerWithParams } from '@/types/api'; - -export const runtime = 'nodejs'; - -export const POST: ApiRouteHandlerWithParams<{ id: string }> = async ( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const { id } = await params; - return proxyToFastAPI(request, `/api/properties/${id}/google/links/import`); -}; diff --git a/web/app/api/properties/[id]/google/links/status/route.ts b/web/app/api/properties/[id]/google/links/status/route.ts deleted file mode 100644 index 0980f6b5..00000000 --- a/web/app/api/properties/[id]/google/links/status/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandlerWithParams } from '@/types/api'; - -export const runtime = 'nodejs'; - -export const GET: ApiRouteHandlerWithParams<{ id: string }> = async ( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const { id } = await params; - return proxyToFastAPI(request, `/api/properties/${id}/google/links/status`); -}; diff --git a/web/app/api/properties/[id]/google/properties/route.ts b/web/app/api/properties/[id]/google/properties/route.ts deleted file mode 100644 index 28f523b2..00000000 --- a/web/app/api/properties/[id]/google/properties/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandlerWithParams } from '@/types/api'; - -export const runtime = 'nodejs'; - -export const GET: ApiRouteHandlerWithParams<{ id: string }> = async ( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const { id } = await params; - return proxyToFastAPI(request, `/api/properties/${id}/google/properties`); -}; diff --git a/web/app/api/properties/[id]/google/status/route.ts b/web/app/api/properties/[id]/google/status/route.ts deleted file mode 100644 index 6cc915e7..00000000 --- a/web/app/api/properties/[id]/google/status/route.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandlerWithParams } from '@/types/api'; - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandlerWithParams<{ id: string }> = async ( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const { id } = await params; - return proxyToFastAPI(request, `/api/properties/${id}/google/status`); -}; diff --git a/web/app/api/properties/[id]/google/test/route.ts b/web/app/api/properties/[id]/google/test/route.ts deleted file mode 100644 index e43c5294..00000000 --- a/web/app/api/properties/[id]/google/test/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandlerWithParams } from '@/types/api'; - -export const runtime = 'nodejs'; - -export const POST: ApiRouteHandlerWithParams<{ id: string }> = async ( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const { id } = await params; - return proxyToFastAPI(request, `/api/properties/${id}/google/test`); -}; diff --git a/web/app/api/properties/[id]/ops/route.ts b/web/app/api/properties/[id]/ops/route.ts deleted file mode 100644 index aec7ebc2..00000000 --- a/web/app/api/properties/[id]/ops/route.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandlerWithParams } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandlerWithParams<{ id: string }> = async ( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const { id } = await params; - return proxyToFastAPI(request, `/api/properties/${id}/ops`); -}; - -export const PUT: ApiRouteHandlerWithParams<{ id: string }> = async ( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const { id } = await params; - return proxyToFastAPI(request, `/api/properties/${id}/ops`); -}; diff --git a/web/app/api/properties/[id]/preset/route.ts b/web/app/api/properties/[id]/preset/route.ts deleted file mode 100644 index 1a01d521..00000000 --- a/web/app/api/properties/[id]/preset/route.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandlerWithParams } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandlerWithParams<{ id: string }> = async ( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const { id } = await params; - return proxyToFastAPI(request, `/api/properties/${id}/preset`); -}; - -export const PUT: ApiRouteHandlerWithParams<{ id: string }> = async ( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const { id } = await params; - return proxyToFastAPI(request, `/api/properties/${id}/preset`); -}; diff --git a/web/app/api/properties/resolve/route.ts b/web/app/api/properties/resolve/route.ts deleted file mode 100644 index 66432d19..00000000 --- a/web/app/api/properties/resolve/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/properties/resolve'); -}; diff --git a/web/app/api/properties/route.ts b/web/app/api/properties/route.ts deleted file mode 100644 index 574b1b7c..00000000 --- a/web/app/api/properties/route.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/properties'); -}; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/properties'); -}; diff --git a/web/app/api/report/audit-tool/route.ts b/web/app/api/report/audit-tool/route.ts deleted file mode 100644 index c8affcd3..00000000 --- a/web/app/api/report/audit-tool/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/report/audit-tool'); -}; diff --git a/web/app/api/report/crawl-payload/route.ts b/web/app/api/report/crawl-payload/route.ts deleted file mode 100644 index 79ffdfc0..00000000 --- a/web/app/api/report/crawl-payload/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - return proxyToFastAPI(request, '/api/report/crawl-payload'); -}; diff --git a/web/app/api/report/export-sitemap/route.ts b/web/app/api/report/export-sitemap/route.ts deleted file mode 100644 index 393882a0..00000000 --- a/web/app/api/report/export-sitemap/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/report/export-sitemap'); -}; diff --git a/web/app/api/report/export-workbook/route.ts b/web/app/api/report/export-workbook/route.ts deleted file mode 100644 index 6999d3d9..00000000 --- a/web/app/api/report/export-workbook/route.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyWorkbookExportToFileService } from '@/server/proxyToFileService'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - - return proxyWorkbookExportToFileService(request); -}; diff --git a/web/app/api/report/export/route.ts b/web/app/api/report/export/route.ts deleted file mode 100644 index f65df5c4..00000000 --- a/web/app/api/report/export/route.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { proxyPdfExportToFileService } from '@/server/proxyToFileService'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - - const format = request.nextUrl.searchParams.get('format') ?? 'csv'; - if (format === 'pdf') { - return proxyPdfExportToFileService(request); - } - - return proxyToFastAPI(request, '/api/report/export'); -}; diff --git a/web/app/api/report/history/route.ts b/web/app/api/report/history/route.ts deleted file mode 100644 index b0eef82d..00000000 --- a/web/app/api/report/history/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - return proxyToFastAPI(request, '/api/report/history'); -}; diff --git a/web/app/api/report/meta/route.ts b/web/app/api/report/meta/route.ts deleted file mode 100644 index 1807e0eb..00000000 --- a/web/app/api/report/meta/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - return proxyToFastAPI(request, '/api/report/meta'); -}; diff --git a/web/app/api/report/mobile-delta/route.ts b/web/app/api/report/mobile-delta/route.ts deleted file mode 100644 index b586c136..00000000 --- a/web/app/api/report/mobile-delta/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - return proxyToFastAPI(request, '/api/report/mobile-delta'); -}; diff --git a/web/app/api/report/payload/route.ts b/web/app/api/report/payload/route.ts deleted file mode 100644 index 38077422..00000000 --- a/web/app/api/report/payload/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/report/payload'); -}; diff --git a/web/app/api/report/portfolio/route.ts b/web/app/api/report/portfolio/route.ts deleted file mode 100644 index 25b34958..00000000 --- a/web/app/api/report/portfolio/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * GET /api/report/portfolio — portfolio groups and crawl history via FastAPI. - */ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - return proxyToFastAPI(request, '/api/report/portfolio'); -}; diff --git a/web/app/api/run/route.ts b/web/app/api/run/route.ts deleted file mode 100644 index 87801773..00000000 --- a/web/app/api/run/route.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * POST /api/run — enqueue a pipeline job via FastAPI. - * FastAPI validates config, saves it, and enqueues to the Python worker. - */ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import { requireApiAuth } from '@/server/auth'; -import type { ApiRouteHandler } from '@/types/api'; - -export const runtime = 'nodejs'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - const authDenied = requireApiAuth(request); - if (authDenied) return authDenied; - return proxyToFastAPI(request, '/api/run'); -}; diff --git a/web/app/api/schedule/check/route.ts b/web/app/api/schedule/check/route.ts deleted file mode 100644 index 7cf4d2b3..00000000 --- a/web/app/api/schedule/check/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const POST: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/schedule/check'); -}; diff --git a/web/app/api/secrets/route.ts b/web/app/api/secrets/route.ts deleted file mode 100644 index 68967b95..00000000 --- a/web/app/api/secrets/route.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { type NextRequest } from 'next/server'; -import { proxyToFastAPI } from '@/server/proxyToFastAPI'; -import { forbiddenIfNotLocal } from '@/server/localOnly'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/secrets'); -}; - -export const PUT: ApiRouteHandler = async (request: NextRequest): Promise => { - const denied = forbiddenIfNotLocal(request); - if (denied) return denied; - return proxyToFastAPI(request, '/api/secrets'); -}; diff --git a/web/app/chat/page.tsx b/web/app/chat/page.tsx deleted file mode 100644 index 3fba8e0c..00000000 --- a/web/app/chat/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Suspense } from 'react'; -import ChatPage from '@/views/Chat'; - -export const dynamic = 'force-dynamic'; - -export default function ChatRoutePage() { - return ( - - - - ); -} diff --git a/web/app/docs/integrations/[slug]/page.tsx b/web/app/docs/integrations/[slug]/page.tsx deleted file mode 100644 index 10efaa77..00000000 --- a/web/app/docs/integrations/[slug]/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { notFound } from 'next/navigation'; -import { isIntegrationGuideSlug } from '@/lib/docs/integrationGuides'; -import DocsIntegrationGuide from '@/views/DocsIntegrationGuide'; - -export const dynamic = 'force-dynamic'; - -export default async function DocsIntegrationRoutePage({ - params, -}: { - params: Promise<{ slug: string }>; -}) { - const { slug } = await params; - if (!isIntegrationGuideSlug(slug)) { - notFound(); - } - return ; -} diff --git a/web/app/docs/page.tsx b/web/app/docs/page.tsx deleted file mode 100644 index 1cebb50a..00000000 --- a/web/app/docs/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import DocsHome from '@/views/DocsHome'; - -export const dynamic = 'force-dynamic'; - -export default function DocsRoutePage() { - return ; -} diff --git a/web/app/favicon.ico b/web/app/favicon.ico deleted file mode 100644 index 718d6fea..00000000 Binary files a/web/app/favicon.ico and /dev/null differ diff --git a/web/app/layout.tsx b/web/app/layout.tsx deleted file mode 100644 index e66a9f2e..00000000 --- a/web/app/layout.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { DM_Sans } from 'next/font/google'; -import type { ReactNode } from 'react'; -import './globals.css'; -import ChunkLoadRecovery from './chunk-load-recovery'; -import ClientProviders from './client-providers'; - -const dmSans = DM_Sans({ - subsets: ['latin'], - weight: ['400', '500', '700'], - variable: '--font-dm-sans', - display: 'swap', -}); - -export const metadata = { - title: 'Site Audit', - description: 'SEO site crawl and audit reports', - icons: { - icon: '/favicon.svg', - apple: '/favicon.svg', - }, -}; - -const themeInit = `(function(){try{ -var v=localStorage.getItem('wp-theme'); -var d=window.matchMedia('(prefers-color-scheme: dark)').matches; -var dark=v==='dark'?true:v==='light'?false:d; -if(dark)document.documentElement.classList.add('dark'); -else document.documentElement.classList.remove('dark'); -document.documentElement.style.colorScheme=dark?'dark':'light'; -var raw=localStorage.getItem('wp-theme-custom:v1'); -if(raw){ - var ct=JSON.parse(raw); - var map=dark?ct.dark:ct.light; - if(map&&typeof map==='object'){ - var el=document.documentElement; - Object.keys(map).forEach(function(k){if(map[k])el.style.setProperty(k,map[k]);}); - } -} -var rp=localStorage.getItem('wp-ui-prefs:v1'); -if(rp){ - var up=JSON.parse(rp); - var rv=up.radius; - var dv=up.density; - var av=up.animations; - var rl=document.documentElement; - var RVARS={'sharp':{'--radius-sm':'0.125rem','--radius-card':'0.25rem','--radius-lg':'0.375rem','--radius-xl':'0.5rem'},'rounded':{'--radius-sm':'0.75rem','--radius-card':'1.25rem','--radius-lg':'1.75rem','--radius-xl':'2rem'},'pill':{'--radius-sm':'999px','--radius-card':'1.75rem','--radius-lg':'2.5rem','--radius-xl':'3rem'}}; - var DVARS={'compact':{'--spacing-page-x':'0.75rem','--spacing-page-y':'0.75rem','--spacing-card':'0.75rem'},'spacious':{'--spacing-page-x':'2.5rem','--spacing-page-y':'2.5rem','--spacing-card':'2rem'}}; - if(RVARS[rv]){Object.keys(RVARS[rv]).forEach(function(k){rl.style.setProperty(k,RVARS[rv][k]);});} - if(DVARS[dv]){Object.keys(DVARS[dv]).forEach(function(k){rl.style.setProperty(k,DVARS[dv][k]);});} - if(av===false){rl.style.setProperty('--dur-fast','0ms');rl.style.setProperty('--dur-base','1ms');rl.style.setProperty('--dur-slow','1ms');} - var fv=up.fontSize; - if(fv==='small')rl.style.setProperty('--font-size-base','15px'); - else if(fv==='large')rl.style.setProperty('--font-size-base','20px'); -} -}catch(e){}})()`.replace(/\n/g, ''); - -export default function RootLayout({ children }: { children: ReactNode }): ReactNode { - return ( - - - + + +
+ + + diff --git a/web/next.config.mjs b/web/next.config.mjs deleted file mode 100644 index dc804c2a..00000000 --- a/web/next.config.mjs +++ /dev/null @@ -1,34 +0,0 @@ -/** @type {import('next').NextConfig} */ - -const nextConfig = { - /** Deploy at site root (`/`). Set `basePath` here if you host under a subpath. */ - env: { - NEXT_PUBLIC_BASE_PATH: '', - }, - async redirects() { - return [ - { - source: '/keywords-explorer', - destination: '/keywords', - permanent: true, - }, - { - source: '/overview', - destination: '/dashboard', - permanent: true, - }, - { - source: '/charts', - destination: '/dashboard?tab=charts', - permanent: false, - }, - { - source: '/content-studio', - destination: '/write', - permanent: true, - }, - ]; - }, -}; - -export default nextConfig; diff --git a/web/nginx.conf b/web/nginx.conf new file mode 100644 index 00000000..40aa59de --- /dev/null +++ b/web/nginx.conf @@ -0,0 +1,31 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + gzip on; + gzip_types text/css application/javascript application/json image/svg+xml; + + location = /keywords-explorer { + return 301 /keywords; + } + location = /overview { + return 301 /dashboard; + } + location = /content-studio { + return 301 /write; + } + location = /charts { + return 301 /dashboard?tab=charts; + } + + location / { + try_files $uri $uri/ /index.html; + } + + location /assets/ { + try_files $uri =404; + add_header Cache-Control "public, max-age=31536000, immutable"; + } +} diff --git a/web/openapi.json b/web/openapi.json index fd126edf..4332aaf6 100644 --- a/web/openapi.json +++ b/web/openapi.json @@ -1410,8 +1410,8 @@ "tags": [ "properties" ], - "summary": "Delete Property", - "operationId": "delete_property_api_properties__property_id__delete", + "summary": "Delete Property Route", + "operationId": "delete_property_route_api_properties__property_id__delete", "parameters": [ { "name": "property_id", @@ -1431,7 +1431,7 @@ "schema": { "type": "object", "additionalProperties": true, - "title": "Response Delete Property Api Properties Property Id Delete" + "title": "Response Delete Property Route Api Properties Property Id Delete" } } } @@ -1454,8 +1454,8 @@ "tags": [ "properties" ], - "summary": "Get Property Ops", - "operationId": "get_property_ops_api_properties__property_id__ops_get", + "summary": "Get Property Ops Route", + "operationId": "get_property_ops_route_api_properties__property_id__ops_get", "parameters": [ { "name": "property_id", @@ -1475,7 +1475,7 @@ "schema": { "type": "object", "additionalProperties": true, - "title": "Response Get Property Ops Api Properties Property Id Ops Get" + "title": "Response Get Property Ops Route Api Properties Property Id Ops Get" } } } @@ -1496,8 +1496,8 @@ "tags": [ "properties" ], - "summary": "Update Property Ops", - "operationId": "update_property_ops_api_properties__property_id__ops_put", + "summary": "Update Property Ops Route", + "operationId": "update_property_ops_route_api_properties__property_id__ops_put", "parameters": [ { "name": "property_id", @@ -1527,7 +1527,7 @@ "schema": { "type": "object", "additionalProperties": true, - "title": "Response Update Property Ops Api Properties Property Id Ops Put" + "title": "Response Update Property Ops Route Api Properties Property Id Ops Put" } } } @@ -1646,9 +1646,8 @@ "tags": [ "properties" ], - "summary": "Authorize Property Crawl", - "description": "Mark property as crawl-authorized (used by OAuth flow).", - "operationId": "authorize_property_crawl_api_properties__property_id__authorize_post", + "summary": "Authorize Property Crawl Route", + "operationId": "authorize_property_crawl_route_api_properties__property_id__authorize_post", "parameters": [ { "name": "property_id", @@ -1668,7 +1667,7 @@ "schema": { "type": "object", "additionalProperties": true, - "title": "Response Authorize Property Crawl Api Properties Property Id Authorize Post" + "title": "Response Authorize Property Crawl Route Api Properties Property Id Authorize Post" } } } @@ -1692,7 +1691,6 @@ "properties" ], "summary": "Property Google Status", - "description": "Return property-level Google integration status.", "operationId": "property_google_status_api_properties__property_id__google_status_get", "parameters": [ { @@ -1737,7 +1735,6 @@ "properties" ], "summary": "Property Google Test", - "description": "Run a quick Google API connectivity test for the property.", "operationId": "property_google_test_api_properties__property_id__google_test_post", "parameters": [ { @@ -1782,7 +1779,6 @@ "properties" ], "summary": "Property Google Properties", - "description": "List GA4 / GSC properties available for this account.", "operationId": "property_google_properties_api_properties__property_id__google_properties_get", "parameters": [ { @@ -1827,7 +1823,6 @@ "properties" ], "summary": "Property Google Links Status", - "description": "Return the status of GSC backlinks import for this property.", "operationId": "property_google_links_status_api_properties__property_id__google_links_status_get", "parameters": [ { @@ -1872,7 +1867,6 @@ "properties" ], "summary": "Property Google Links Import", - "description": "Trigger a GSC backlinks import for this property.", "operationId": "property_google_links_import_api_properties__property_id__google_links_import_post", "parameters": [ { @@ -1917,7 +1911,6 @@ "properties" ], "summary": "Patch Property Google Credentials", - "description": "Update Google credentials/settings for a property (used by OAuth callback).", "operationId": "patch_property_google_credentials_api_properties__property_id__google_credentials_patch", "parameters": [ { @@ -1970,7 +1963,6 @@ "properties" ], "summary": "Post Property Google Credentials", - "description": "Update Google site/property settings from the integrations UI.", "operationId": "post_property_google_credentials_api_properties__property_id__google_credentials_post", "parameters": [ { @@ -2025,7 +2017,6 @@ "properties" ], "summary": "Post Property Google Disconnect", - "description": "Clear OAuth tokens for a property.", "operationId": "post_property_google_disconnect_api_properties__property_id__google_disconnect_post", "parameters": [ { @@ -2479,13 +2470,14 @@ } } }, - "/api/integrations/google/status": { + "/api/integrations/google/credentials": { "get": { "tags": [ "integrations" ], - "summary": "Google Status", - "operationId": "google_status_api_integrations_google_status_get", + "summary": "Get Google Credentials", + "description": "Full app-level Google OAuth settings (server-side / local admin only).", + "operationId": "get_google_credentials_api_integrations_google_credentials_get", "responses": { "200": { "description": "Successful Response", @@ -2494,15 +2486,13 @@ "schema": { "additionalProperties": true, "type": "object", - "title": "Response Google Status Api Integrations Google Status Get" + "title": "Response Get Google Credentials Api Integrations Google Credentials Get" } } } } } - } - }, - "/api/integrations/google/credentials": { + }, "post": { "tags": [ "integrations" @@ -2547,6 +2537,29 @@ } } }, + "/api/integrations/google/status": { + "get": { + "tags": [ + "integrations" + ], + "summary": "Google Status", + "operationId": "google_status_api_integrations_google_status_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object", + "title": "Response Google Status Api Integrations Google Status Get" + } + } + } + } + } + } + }, "/api/integrations/google/credentials/upload": { "post": { "tags": [ @@ -2616,6 +2629,168 @@ } } }, + "/api/integrations/google/auth": { + "get": { + "tags": [ + "integrations" + ], + "summary": "Google Oauth Start", + "operationId": "google_oauth_start_api_integrations_google_auth_get", + "parameters": [ + { + "name": "propertyId", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Propertyid" + } + }, + { + "name": "startUrl", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Starturl" + } + }, + { + "name": "returnTo", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Returnto" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Google Oauth Start Api Integrations Google Auth Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/integrations/google/callback": { + "get": { + "tags": [ + "integrations" + ], + "summary": "Google Oauth Callback", + "operationId": "google_oauth_callback_api_integrations_google_callback_get", + "parameters": [ + { + "name": "code", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Code" + } + }, + { + "name": "state", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "State" + } + }, + { + "name": "error", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Google Oauth Callback Api Integrations Google Callback Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/api/integrations/google/properties": { "get": { "tags": [ @@ -3376,8 +3551,8 @@ "tags": [ "issues" ], - "summary": "List Issue Status", - "operationId": "list_issue_status_api_issues_status_get", + "summary": "List Issue Status Route", + "operationId": "list_issue_status_route_api_issues_status_get", "parameters": [ { "name": "propertyId", @@ -3397,7 +3572,7 @@ "schema": { "type": "object", "additionalProperties": true, - "title": "Response List Issue Status Api Issues Status Get" + "title": "Response List Issue Status Route Api Issues Status Get" } } } @@ -3418,8 +3593,8 @@ "tags": [ "issues" ], - "summary": "Upsert Issue Status", - "operationId": "upsert_issue_status_api_issues_status_put", + "summary": "Upsert Issue Status Route", + "operationId": "upsert_issue_status_route_api_issues_status_put", "requestBody": { "content": { "application/json": { @@ -3440,7 +3615,7 @@ "schema": { "type": "object", "additionalProperties": true, - "title": "Response Upsert Issue Status Api Issues Status Put" + "title": "Response Upsert Issue Status Route Api Issues Status Put" } } } @@ -3951,8 +4126,8 @@ "tags": [ "content" ], - "summary": "List Content Drafts", - "operationId": "list_content_drafts_api_content_drafts_get", + "summary": "List Content Drafts Route", + "operationId": "list_content_drafts_route_api_content_drafts_get", "parameters": [ { "name": "propertyId", @@ -3972,7 +4147,7 @@ "schema": { "type": "object", "additionalProperties": true, - "title": "Response List Content Drafts Api Content Drafts Get" + "title": "Response List Content Drafts Route Api Content Drafts Get" } } } @@ -3993,8 +4168,8 @@ "tags": [ "content" ], - "summary": "Create Content Draft", - "operationId": "create_content_draft_api_content_drafts_post", + "summary": "Create Content Draft Route", + "operationId": "create_content_draft_route_api_content_drafts_post", "requestBody": { "content": { "application/json": { @@ -4015,7 +4190,7 @@ "schema": { "type": "object", "additionalProperties": true, - "title": "Response Create Content Draft Api Content Drafts Post" + "title": "Response Create Content Draft Route Api Content Drafts Post" } } } @@ -4038,8 +4213,8 @@ "tags": [ "content" ], - "summary": "Get Content Draft", - "operationId": "get_content_draft_api_content_drafts__draft_id__get", + "summary": "Get Content Draft Route", + "operationId": "get_content_draft_route_api_content_drafts__draft_id__get", "parameters": [ { "name": "draft_id", @@ -4059,7 +4234,7 @@ "schema": { "type": "object", "additionalProperties": true, - "title": "Response Get Content Draft Api Content Drafts Draft Id Get" + "title": "Response Get Content Draft Route Api Content Drafts Draft Id Get" } } } @@ -4080,8 +4255,8 @@ "tags": [ "content" ], - "summary": "Update Content Draft", - "operationId": "update_content_draft_api_content_drafts__draft_id__patch", + "summary": "Update Content Draft Route", + "operationId": "update_content_draft_route_api_content_drafts__draft_id__patch", "parameters": [ { "name": "draft_id", @@ -4113,7 +4288,7 @@ "schema": { "type": "object", "additionalProperties": true, - "title": "Response Update Content Draft Api Content Drafts Draft Id Patch" + "title": "Response Update Content Draft Route Api Content Drafts Draft Id Patch" } } } @@ -4134,8 +4309,8 @@ "tags": [ "content" ], - "summary": "Delete Content Draft", - "operationId": "delete_content_draft_api_content_drafts__draft_id__delete", + "summary": "Delete Content Draft Route", + "operationId": "delete_content_draft_route_api_content_drafts__draft_id__delete", "parameters": [ { "name": "draft_id", @@ -4155,7 +4330,7 @@ "schema": { "type": "object", "additionalProperties": true, - "title": "Response Delete Content Draft Api Content Drafts Draft Id Delete" + "title": "Response Delete Content Draft Route Api Content Drafts Draft Id Delete" } } } @@ -4178,8 +4353,8 @@ "tags": [ "page-markdown" ], - "summary": "List Page Markdown", - "operationId": "list_page_markdown_api_page_markdown_get", + "summary": "List Page Markdown Route", + "operationId": "list_page_markdown_route_api_page_markdown_get", "parameters": [ { "name": "crawlRunId", @@ -4238,7 +4413,7 @@ "schema": { "type": "object", "additionalProperties": true, - "title": "Response List Page Markdown Api Page Markdown Get" + "title": "Response List Page Markdown Route Api Page Markdown Get" } } } @@ -4259,8 +4434,8 @@ "tags": [ "page-markdown" ], - "summary": "Delete Page Markdown", - "operationId": "delete_page_markdown_api_page_markdown_delete", + "summary": "Delete Page Markdown Route", + "operationId": "delete_page_markdown_route_api_page_markdown_delete", "requestBody": { "content": { "application/json": { @@ -4281,7 +4456,7 @@ "schema": { "type": "object", "additionalProperties": true, - "title": "Response Delete Page Markdown Api Page Markdown Delete" + "title": "Response Delete Page Markdown Route Api Page Markdown Delete" } } } @@ -4304,8 +4479,8 @@ "tags": [ "page-markdown" ], - "summary": "Page Markdown Content", - "operationId": "page_markdown_content_api_page_markdown_content_get", + "summary": "Page Markdown Content Route", + "operationId": "page_markdown_content_route_api_page_markdown_content_get", "parameters": [ { "name": "crawlRunId", @@ -4334,7 +4509,7 @@ "schema": { "type": "object", "additionalProperties": true, - "title": "Response Page Markdown Content Api Page Markdown Content Get" + "title": "Response Page Markdown Content Route Api Page Markdown Content Get" } } } @@ -4402,8 +4577,8 @@ "tags": [ "page-markdown" ], - "summary": "Page Markdown Runs", - "operationId": "page_markdown_runs_api_page_markdown_runs_get", + "summary": "Page Markdown Runs Route", + "operationId": "page_markdown_runs_route_api_page_markdown_runs_get", "parameters": [ { "name": "propertyId", @@ -4430,7 +4605,7 @@ "schema": { "type": "object", "additionalProperties": true, - "title": "Response Page Markdown Runs Api Page Markdown Runs Get" + "title": "Response Page Markdown Runs Route Api Page Markdown Runs Get" } } } @@ -4876,53 +5051,6 @@ } } }, - "/api/report/export-workbook": { - "get": { - "tags": [ - "report-export" - ], - "summary": "Export Workbook", - "operationId": "export_workbook_api_report_export_workbook_get", - "parameters": [ - { - "name": "reportId", - "in": "query", - "required": false, - "schema": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Reportid" - } - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, "/api/report/portfolio": { "get": { "tags": [ diff --git a/web/package-lock.json b/web/package-lock.json index 0b70d3db..1e08c7d2 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -12,6 +12,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@fontsource-variable/dm-sans": "^5.2.5", "@tanstack/react-virtual": "^3.13.23", "@tiptap/extension-image": "^3.26.1", "@tiptap/extension-link": "^3.26.1", @@ -32,28 +33,34 @@ "d3": "^7.9.0", "echarts": "^6.1.0", "lucide-react": "^0.577.0", - "next": "15.5.14", "react": "19.1.0", "react-chartjs-2": "^5.3.1", "react-dom": "19.1.0", + "react-draggable": "4.5.0", "react-grid-layout": "^2.2.3", "react-markdown": "^10.1.0", + "react-router-dom": "^7.6.2", "react-syntax-highlighter": "^16.1.1", "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.1" }, "devDependencies": { - "@eslint/eslintrc": "^3", + "@eslint/js": "^9", "@hey-api/openapi-ts": "^0.98.2", "@tailwindcss/postcss": "^4", + "@tailwindcss/vite": "^4.1.8", "@types/d3": "^7.4.3", "@types/node": "^25.9.1", "@types/react": "^19.2.16", "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.5.2", "eslint": "^9", - "eslint-config-next": "15.5.14", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", "tailwindcss": "^4", "typescript": "^6.0.3", + "typescript-eslint": "^8.34.0", + "vite": "^6.3.5", "vitest": "^3.2.6" } }, @@ -70,6 +77,273 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/runtime": { "version": "7.29.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", @@ -79,6 +353,54 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@dnd-kit/accessibility": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", @@ -148,6 +470,7 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -166,9 +489,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -183,9 +506,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -200,9 +523,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -217,9 +540,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -234,9 +557,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -251,9 +574,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -268,9 +591,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -285,9 +608,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -302,9 +625,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -319,9 +642,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -336,9 +659,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -353,9 +676,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -370,9 +693,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -387,9 +710,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -404,9 +727,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -421,9 +744,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -438,9 +761,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -455,9 +778,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], @@ -472,9 +795,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -489,9 +812,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], @@ -506,9 +829,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -523,9 +846,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ "arm64" ], @@ -540,9 +863,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -557,9 +880,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -574,9 +897,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -591,9 +914,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -779,6 +1102,15 @@ "license": "MIT", "optional": true }, + "node_modules/@fontsource-variable/dm-sans": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource-variable/dm-sans/-/dm-sans-5.2.8.tgz", + "integrity": "sha512-AxkvMTvNWgfrmlyjiV05vlHYJa+nRQCf1EfvIrQAPBpFJW0O9VTz7oAFr9S3lvbWdmnFoBk7yFqQL86u64nl2g==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@hey-api/codegen-core": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.9.0.tgz", @@ -952,510 +1284,44 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "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", - "optional": true, - "engines": { - "node": ">=18" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "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/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], + "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": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "node": ">=6.0.0" } }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "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/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", @@ -1504,197 +1370,12 @@ "@tybys/wasm-util": "^0.10.0" } }, - "node_modules/@next/env": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.14.tgz", - "integrity": "sha512-aXeirLYuASxEgi4X4WhfXsShCFxWDfNn/8ZeC5YXAS2BB4A8FJi1kwwGL6nvMVboE7fZCzmJPNdMvVHc8JpaiA==", - "license": "MIT" - }, - "node_modules/@next/eslint-plugin-next": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.14.tgz", - "integrity": "sha512-ogBjgsFrPPz19abP3VwcYSahbkUOMMvJjxCOYWYndw+PydeMuLuB4XrvNkNutFrTjC9St2KFULRdKID8Sd/CMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-glob": "3.3.1" - } - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.14.tgz", - "integrity": "sha512-Y9K6SPzobnZvrRDPO2s0grgzC+Egf0CqfbdvYmQVaztV890zicw8Z8+4Vqw8oPck8r1TjUHxVh8299Cg4TrxXg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.14.tgz", - "integrity": "sha512-aNnkSMjSFRTOmkd7qoNI2/rETQm/vKD6c/Ac9BZGa9CtoOzy3c2njgz7LvebQJ8iPxdeTuGnAjagyis8a9ifBw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.14.tgz", - "integrity": "sha512-tjlpia+yStPRS//6sdmlVwuO1Rioern4u2onafa5n+h2hCS9MAvMXqpVbSrjgiEOoCs0nJy7oPOmWgtRRNSM5Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.14.tgz", - "integrity": "sha512-8B8cngBaLadl5lbDRdxGCP1Lef8ipD6KlxS3v0ElDAGil6lafrAM3B258p1KJOglInCVFUjk751IXMr2ixeQOQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.14.tgz", - "integrity": "sha512-bAS6tIAg8u4Gn3Nz7fCPpSoKAexEt2d5vn1mzokcqdqyov6ZJ6gu6GdF9l8ORFrBuRHgv3go/RfzYz5BkZ6YSQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.14.tgz", - "integrity": "sha512-mMxv/FcrT7Gfaq4tsR22l17oKWXZmH/lVqcvjX0kfp5I0lKodHYLICKPoX1KRnnE+ci6oIUdriUhuA3rBCDiSw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.14.tgz", - "integrity": "sha512-OTmiBlYThppnvnsqx0rBqjDRemlmIeZ8/o4zI7veaXoeO1PVHoyj2lfTfXTiiGjCyRDhA10y4h6ZvZvBiynr2g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.14.tgz", - "integrity": "sha512-+W7eFf3RS7m4G6tppVTOSyP9Y6FsJXfOuKzav1qKniiFm3KFByQfPEcouHdjlZmysl4zJGuGLQ/M9XyVeyeNEg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nolyfill/is-core-module": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", - "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.4.0" - } + "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.61.0", @@ -2085,29 +1766,6 @@ "win32" ] }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.16.1.tgz", - "integrity": "sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, "node_modules/@tailwindcss/node": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", @@ -2199,10 +1857,296 @@ "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", - "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "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.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", + "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "postcss": "^8.5.6", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.1.tgz", + "integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.1", + "@tailwindcss/oxide": "4.3.1", + "tailwindcss": "4.3.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/node": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz", + "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==", + "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.1" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz", + "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-x64": "4.3.1", + "@tailwindcss/oxide-freebsd-x64": "4.3.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-x64-musl": "4.3.1", + "@tailwindcss/oxide-wasm32-wasi": "4.3.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz", + "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz", + "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz", + "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz", + "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==", "cpu": [ "x64" ], @@ -2216,10 +2160,10 @@ "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", - "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz", + "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==", "cpu": [ "arm" ], @@ -2233,14 +2177,17 @@ "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", - "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz", + "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2250,14 +2197,17 @@ "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", - "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz", + "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2267,14 +2217,17 @@ "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", - "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz", + "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2284,14 +2237,17 @@ "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", - "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz", + "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2301,10 +2257,10 @@ "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", - "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz", + "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -2320,21 +2276,21 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.8.1", - "@emnapi/runtime": "^1.8.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.1", - "@tybys/wasm-util": "^0.10.1", + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@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.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", - "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz", + "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==", "cpu": [ "arm64" ], @@ -2348,10 +2304,10 @@ "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", - "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz", + "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==", "cpu": [ "x64" ], @@ -2365,19 +2321,12 @@ "node": ">= 20" } }, - "node_modules/@tailwindcss/postcss": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", - "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", + "node_modules/@tailwindcss/vite/node_modules/tailwindcss": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz", + "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==", "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.2.2", - "@tailwindcss/oxide": "4.2.2", - "postcss": "^8.5.6", - "tailwindcss": "4.2.2" - } + "license": "MIT" }, "node_modules/@tanstack/react-virtual": { "version": "3.13.23", @@ -2961,6 +2910,51 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -3310,13 +3304,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -3379,17 +3366,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", - "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==", + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.0.tgz", + "integrity": "sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/type-utils": "8.60.1", - "@typescript-eslint/utils": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", + "@typescript-eslint/scope-manager": "8.62.0", + "@typescript-eslint/type-utils": "8.62.0", + "@typescript-eslint/utils": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -3402,7 +3389,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.60.1", + "@typescript-eslint/parser": "^8.62.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -3418,16 +3405,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", - "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.0.tgz", + "integrity": "sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", + "@typescript-eslint/scope-manager": "8.62.0", + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0", "debug": "^4.4.3" }, "engines": { @@ -3443,14 +3430,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", - "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.0.tgz", + "integrity": "sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.60.1", - "@typescript-eslint/types": "^8.60.1", + "@typescript-eslint/tsconfig-utils": "^8.62.0", + "@typescript-eslint/types": "^8.62.0", "debug": "^4.4.3" }, "engines": { @@ -3465,14 +3452,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", - "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==", + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.0.tgz", + "integrity": "sha512-1lX38kNxXIRb8mEc3lbq5mdHq1Pf2+U0nFU65KfT18mtPxxl0fvjuEE92mHuXPuCtElJhOrddOpyMlM3Z0umEA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1" + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3483,9 +3470,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", - "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz", + "integrity": "sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==", "dev": true, "license": "MIT", "engines": { @@ -3500,15 +3487,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", - "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.0.tgz", + "integrity": "sha512-+g5O3j0w2ldzC86Pv6fvbO/xhAonbJFIdf/MKQ1d30gndlsVzUOE83ldfSE15Qrl9fhFjK6AovHs5Wpp6vx86w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1", - "@typescript-eslint/utils": "8.60.1", + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0", + "@typescript-eslint/utils": "8.62.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -3525,9 +3512,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", - "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.0.tgz", + "integrity": "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==", "dev": true, "license": "MIT", "engines": { @@ -3539,16 +3526,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", - "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.0.tgz", + "integrity": "sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.60.1", - "@typescript-eslint/tsconfig-utils": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", + "@typescript-eslint/project-service": "8.62.0", + "@typescript-eslint/tsconfig-utils": "8.62.0", + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -3606,16 +3593,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", - "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.0.tgz", + "integrity": "sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1" + "@typescript-eslint/scope-manager": "8.62.0", + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3630,13 +3617,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", - "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.0.tgz", + "integrity": "sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/types": "8.62.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -3664,277 +3651,29 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "license": "ISC" - }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" }, "engines": { - "node": ">=14.0.0" + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@vitest/expect": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", @@ -4148,176 +3887,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -4328,59 +3897,6 @@ "node": ">=12" } }, - "node_modules/ast-types-flow": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axe-core": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", - "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", - "dev": true, - "license": "MPL-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -4398,6 +3914,19 @@ "dev": true, "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.38", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", @@ -4409,17 +3938,38 @@ "concat-map": "0.0.1" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" }, "engines": { - "node": ">=8" + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, "node_modules/bundle-name": { @@ -4477,56 +4027,6 @@ "node": ">=8" } }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4538,9 +4038,10 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001781", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", - "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -4679,12 +4180,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -4757,6 +4252,26 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5204,80 +4719,19 @@ "d3-transition": "2 - 3" }, "engines": { - "node": ">=12" - } - }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/data-bind-mapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/data-bind-mapper/-/data-bind-mapper-1.0.3.tgz", - "integrity": "sha512-QmU3lyEnbENQPo0M1F9BMu4s6cqNNp8iJA+b/HP2sSb7pf3dxwF3+EP1eO69rwBfH9kFJ1apmzrtogAmVt2/Xw==", - "license": "MIT", - "dependencies": { - "accessor-fn": "1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" + "node": ">=12" } }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, + "node_modules/data-bind-mapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/data-bind-mapper/-/data-bind-mapper-1.0.3.tgz", + "integrity": "sha512-QmU3lyEnbENQPo0M1F9BMu4s6cqNNp8iJA+b/HP2sSb7pf3dxwF3+EP1eO69rwBfH9kFJ1apmzrtogAmVt2/Xw==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "accessor-fn": "1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=12" } }, "node_modules/debug": { @@ -5357,24 +4811,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", @@ -5388,24 +4824,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/defu": { "version": "6.1.7", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", @@ -5442,7 +4860,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -5461,19 +4879,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/dotenv": { "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", @@ -5487,21 +4892,6 @@ "url": "https://dotenvx.com" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/echarts": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.1.0.tgz", @@ -5518,145 +4908,27 @@ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", "license": "0BSD" }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "node_modules/electron-to-chromium": { + "version": "1.5.377", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.377.tgz", + "integrity": "sha512-cH1jZgJHoezfTnKfKwnScpHywTFVnJUNITDPREFdhNjiuD502+QFpG0Qk7G8jhsV/f+CEAFlIrzP1fT+IMb92g==", "dev": true, - "license": "MIT" + "license": "ISC" }, "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "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.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" } }, - "node_modules/es-abstract": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", - "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.1.tgz", - "integrity": "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.1", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.1.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.3.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.5", - "math-intrinsics": "^1.1.0", - "safe-array-concat": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -5664,70 +4936,10 @@ "dev": true, "license": "MIT" }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5735,338 +4947,118 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-next": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.14.tgz", - "integrity": "sha512-lmJ5F8ZgOYogq0qtH4L5SpxuASY2SPdOzqUprN2/56+P3GPsIpXaUWIJC66kYIH+yZdsM4nkHE5MIBP6s1NiBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@next/eslint-plugin-next": "15.5.14", - "@rushstack/eslint-patch": "^1.10.3", - "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jsx-a11y": "^6.10.0", - "eslint-plugin-react": "^7.37.0", - "eslint-plugin-react-hooks": "^5.0.0" - }, - "peerDependencies": { - "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", - "typescript": ">=3.3.1" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-import-resolver-typescript": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", - "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@nolyfill/is-core-module": "1.0.39", - "debug": "^4.4.0", - "get-tsconfig": "^4.10.0", - "is-bun-module": "^2.0.0", - "stable-hash": "^0.0.5", - "tinyglobby": "^0.2.13", - "unrs-resolver": "^1.6.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-import-resolver-typescript" - }, - "peerDependencies": { - "eslint": "*", - "eslint-plugin-import": "*", - "eslint-plugin-import-x": "*" - }, - "peerDependenciesMeta": { - "eslint-plugin-import": { - "optional": true - }, - "eslint-plugin-import-x": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.32.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", - "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.9", - "array.prototype.findlastindex": "^1.2.6", - "array.prototype.flat": "^1.3.3", - "array.prototype.flatmap": "^1.3.3", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.1", - "hasown": "^2.0.2", - "is-core-module": "^2.16.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.1", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.9", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "MIT", + "engines": { + "node": ">=6" } }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", - "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", - "dependencies": { - "aria-query": "^5.3.2", - "array-includes": "^3.1.8", - "array.prototype.flatmap": "^1.3.2", - "ast-types-flow": "^0.0.8", - "axe-core": "^4.10.0", - "axobject-query": "^4.1.0", - "damerau-levenshtein": "^1.0.8", - "emoji-regex": "^9.2.2", - "hasown": "^2.0.2", - "jsx-ast-utils": "^3.3.5", - "language-tags": "^1.0.9", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.1" - }, "engines": { - "node": ">=4.0" + "node": ">=10" }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" }, "engines": { - "node": ">=4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-plugin-react-hooks": { @@ -6082,38 +5074,14 @@ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", - "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", "dev": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "node-exports-info": "^1.6.0", - "object-keys": "^1.1.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "peerDependencies": { + "eslint": ">=8.40" } }, "node_modules/eslint-scope": { @@ -6269,36 +5237,6 @@ "node": ">=6.0.0" } }, - "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -6313,16 +5251,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/fault": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", @@ -6349,19 +5277,6 @@ "node": ">=16.0.0" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6414,22 +5329,6 @@ "node": ">=12" } }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", @@ -6453,112 +5352,14 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6.9.0" } }, "node_modules/get-tsconfig": { @@ -6610,135 +5411,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/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/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, "engines": { - "node": ">= 0.4" + "node": ">=8" } }, "node_modules/hast-util-parse-selector": { @@ -6891,21 +5578,6 @@ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", "license": "MIT" }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -6939,151 +5611,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bun-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", - "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.7.1" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-decimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", @@ -7120,305 +5647,71 @@ "node": ">=0.10.0" } }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-in-ssh": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", - "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=20" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3" + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" }, "engines": { - "node": ">= 0.4" + "node": ">=14.16" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, "engines": { - "node": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-wsl": { @@ -7437,13 +5730,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -7451,28 +5737,10 @@ "dev": true, "license": "ISC" }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "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": { @@ -7498,6 +5766,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -7519,35 +5800,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, "node_modules/kapsule": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.3.tgz", @@ -7570,26 +5822,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/language-tags": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", - "dev": true, - "license": "MIT", - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -7943,6 +6175,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, "node_modules/lucide-react": { "version": "0.577.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", @@ -7984,16 +6226,6 @@ "node": ">= 20" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", @@ -8290,16 +6522,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -8863,20 +7085,6 @@ ], "license": "MIT" }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -8890,16 +7098,6 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -8910,6 +7108,7 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, "funding": [ { "type": "github", @@ -8924,22 +7123,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/napi-postinstall": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", - "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", - "dev": true, - "license": "MIT", - "bin": { - "napi-postinstall": "lib/cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/napi-postinstall" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -8947,86 +7130,6 @@ "dev": true, "license": "MIT" }, - "node_modules/next": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.14.tgz", - "integrity": "sha512-M6S+4JyRjmKic2Ssm7jHUPkE6YUJ6lv4507jprsSZLulubz0ihO2E+S4zmQK3JZ2ov81JrugukKU4Tz0ivgqqQ==", - "license": "MIT", - "dependencies": { - "@next/env": "15.5.14", - "@swc/helpers": "0.5.15", - "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", - "styled-jsx": "5.1.6" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.14", - "@next/swc-darwin-x64": "15.5.14", - "@next/swc-linux-arm64-gnu": "15.5.14", - "@next/swc-linux-arm64-musl": "15.5.14", - "@next/swc-linux-x64-gnu": "15.5.14", - "@next/swc-linux-x64-musl": "15.5.14", - "@next/swc-win32-arm64-msvc": "15.5.14", - "@next/swc-win32-x64-msvc": "15.5.14", - "sharp": "^0.34.3" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.51.1", - "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/ngraph.events": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.4.0.tgz", @@ -9055,165 +7158,33 @@ }, "node_modules/ngraph.merge": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ngraph.merge/-/ngraph.merge-1.0.0.tgz", - "integrity": "sha512-5J8YjGITUJeapsomtTALYsw7rFveYkM+lBj3QiYZ79EymQcuri65Nw3knQtFxQBU1r5iOaVRXrSwMENUPK62Vg==", - "license": "MIT" - }, - "node_modules/ngraph.random": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ngraph.random/-/ngraph.random-1.2.0.tgz", - "integrity": "sha512-4EUeAGbB2HWX9njd6bP6tciN6ByJfoaAvmVL9QTaZSeXrW46eNGA9GajiXiPBbvFqxUWFkEbyo6x5qsACUuVfA==", - "license": "BSD-3-Clause" - }, - "node_modules/node-exports-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", - "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "array.prototype.flatmap": "^1.3.3", - "es-errors": "^1.3.0", - "object.entries": "^1.1.9", - "semver": "^6.3.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/node-exports-info/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "resolved": "https://registry.npmjs.org/ngraph.merge/-/ngraph.merge-1.0.0.tgz", + "integrity": "sha512-5J8YjGITUJeapsomtTALYsw7rFveYkM+lBj3QiYZ79EymQcuri65Nw3knQtFxQBU1r5iOaVRXrSwMENUPK62Vg==", + "license": "MIT" }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "node_modules/ngraph.random": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ngraph.random/-/ngraph.random-1.2.0.tgz", + "integrity": "sha512-4EUeAGbB2HWX9njd6bP6tciN6ByJfoaAvmVL9QTaZSeXrW46eNGA9GajiXiPBbvFqxUWFkEbyo6x5qsACUuVfA==", + "license": "BSD-3-Clause" + }, + "node_modules/node-releases": { + "version": "2.0.48", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz", + "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, "node_modules/ohash": { @@ -9268,24 +7239,6 @@ "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", "license": "MIT" }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -9376,13 +7329,6 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -9411,20 +7357,8 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } + "license": "ISC" }, "node_modules/pkg-types": { "version": "2.3.1", @@ -9450,16 +7384,6 @@ "node": ">=10" } }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", @@ -9701,27 +7625,6 @@ "node": ">=6" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/rc9": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz", @@ -9765,9 +7668,9 @@ } }, "node_modules/react-draggable": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.7.0.tgz", - "integrity": "sha512-kTpANmKWVnFXiZ76Ag2ZowiFStuBYnJ606PI1TbUsOg29/400/JNIxI9+CuenhiAqFuXWJffz6F4UI3R51kUug==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz", + "integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==", "license": "MIT", "dependencies": { "clsx": "^2.1.1", @@ -9835,6 +7738,16 @@ "react": ">=18" } }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-resizable": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.2.0.tgz", @@ -9849,6 +7762,44 @@ "react-dom": ">= 16.3" } }, + "node_modules/react-router": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz", + "integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==", + "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.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz", + "integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/react-syntax-highlighter": { "version": "16.1.1", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz", @@ -9883,29 +7834,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/refractor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz", @@ -9922,27 +7850,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/remark-breaks": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz", @@ -10030,27 +7937,6 @@ "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", "license": "MIT" }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -10071,17 +7957,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/robust-predicates": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", @@ -10152,91 +8027,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/rw": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", "license": "BSD-3-Clause" }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -10246,213 +8042,49 @@ "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", - "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", - "devOptional": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=10" } }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "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/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, "node_modules/siginfo": { @@ -10466,6 +8098,7 @@ "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" @@ -10481,13 +8114,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/stable-hash": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", - "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", - "dev": true, - "license": "MIT" - }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -10502,133 +8128,6 @@ "dev": true, "license": "MIT" }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string.prototype.includes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", - "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -10643,16 +8142,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -10704,29 +8193,6 @@ "inline-style-parser": "0.2.7" } }, - "node_modules/styled-jsx": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", - "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", - "license": "MIT", - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -10740,19 +8206,6 @@ "node": ">=8" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/tailwindcss": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", @@ -10761,9 +8214,9 @@ "license": "MIT" }, "node_modules/tapable": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", - "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "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": { @@ -10921,19 +8374,6 @@ "node": ">=14.0.0" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -10967,19 +8407,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -10999,84 +8426,6 @@ "node": ">= 0.8.0" } }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", @@ -11091,23 +8440,28 @@ "node": ">=14.17" } }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "node_modules/typescript-eslint": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.0.tgz", + "integrity": "sha512-8QxXi+ZACKX0kaqO4gY8kn0RSD9gFfaHDWwjqtEN48aWCBkX4MJaufWN+c3BzlrXLOxfywDL8CaoqUwcRq4j4Q==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" + "@typescript-eslint/eslint-plugin": "8.62.0", + "@typescript-eslint/parser": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0", + "@typescript-eslint/utils": "8.62.0" }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/undici-types": { @@ -11204,39 +8558,35 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unrs-resolver": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, - "hasInstallScript": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "napi-postinstall": "^0.3.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, - "funding": { - "url": "https://opencollective.com/unrs-resolver" + "bin": { + "update-browserslist-db": "cli.js" }, - "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.11.1", - "@unrs/resolver-binding-android-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-x64": "1.11.1", - "@unrs/resolver-binding-freebsd-x64": "1.11.1", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", - "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-musl": "1.11.1", - "@unrs/resolver-binding-wasm32-wasi": "1.11.1", - "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", - "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", - "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, "node_modules/uri-js": { @@ -11287,24 +8637,24 @@ } }, "node_modules/vite": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", - "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -11313,14 +8663,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", - "less": "^4.0.0", + "less": "*", "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -11523,95 +8873,6 @@ "node": ">= 8" } }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.20", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", - "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -11656,6 +8917,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/web/package.json b/web/package.json index 2f4ce426..befcd7b1 100644 --- a/web/package.json +++ b/web/package.json @@ -3,10 +3,12 @@ "version": "0.1.0", "private": true, "license": "MIT", + "type": "module", "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "start": "vite preview --host 0.0.0.0 --port 3000", "lint": "eslint", "test": "vitest run", "typecheck": "tsc --noEmit", @@ -18,6 +20,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@fontsource-variable/dm-sans": "^5.2.5", "@tanstack/react-virtual": "^3.13.23", "@tiptap/extension-image": "^3.26.1", "@tiptap/extension-link": "^3.26.1", @@ -38,28 +41,37 @@ "d3": "^7.9.0", "echarts": "^6.1.0", "lucide-react": "^0.577.0", - "next": "15.5.14", "react": "19.1.0", "react-chartjs-2": "^5.3.1", "react-dom": "19.1.0", + "react-draggable": "4.5.0", "react-grid-layout": "^2.2.3", "react-markdown": "^10.1.0", + "react-router-dom": "^7.6.2", "react-syntax-highlighter": "^16.1.1", "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.1" }, "devDependencies": { - "@eslint/eslintrc": "^3", + "@eslint/js": "^9", "@hey-api/openapi-ts": "^0.98.2", "@tailwindcss/postcss": "^4", + "@tailwindcss/vite": "^4.1.8", "@types/d3": "^7.4.3", "@types/node": "^25.9.1", "@types/react": "^19.2.16", "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.5.2", "eslint": "^9", - "eslint-config-next": "15.5.14", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", "tailwindcss": "^4", "typescript": "^6.0.3", + "typescript-eslint": "^8.34.0", + "vite": "^6.3.5", "vitest": "^3.2.6" + }, + "overrides": { + "react-draggable": "4.5.0" } } diff --git a/web/postcss.config.mjs b/web/postcss.config.mjs deleted file mode 100644 index c7bcb4b1..00000000 --- a/web/postcss.config.mjs +++ /dev/null @@ -1,5 +0,0 @@ -const config = { - plugins: ["@tailwindcss/postcss"], -}; - -export default config; diff --git a/web/src/AppRoutes.tsx b/web/src/AppRoutes.tsx new file mode 100644 index 00000000..47d15354 --- /dev/null +++ b/web/src/AppRoutes.tsx @@ -0,0 +1,73 @@ +import { Suspense } from 'react'; +import { Navigate, Route, Routes } from 'react-router-dom'; +import LandingPage from '@/views/Landing'; +import ChatPage from '@/views/Chat'; +import DocsHome from '@/views/DocsHome'; +import McpSettings from '@/views/McpSettings'; +import PagesMarkdown from '@/views/PagesMarkdown'; +import Pipeline from '@/views/Pipeline'; +import RiskSettings from '@/views/RiskSettings'; +import Secrets from '@/views/Secrets'; +import Settings from '@/views/Settings'; +import WriteStudio from '@/views/WriteStudio'; +import ReportLayout from '@/layouts/ReportLayout'; +import ReportSlugPage from '@/pages/ReportSlugPage'; +import ReportsNotFoundPage from '@/pages/ReportsNotFoundPage'; +import NotFoundPage from '@/pages/NotFoundPage'; +import DocsIntegrationRoutePage from '@/pages/DocsIntegrationRoutePage'; +import { strings } from '@/lib/strings'; +import { usePageTitle } from '@/hooks/usePageTitle'; + +function LandingRoute() { + usePageTitle(strings.views.landing.metaTitle); + return ; +} + +function ChatRoute() { + return ( + + + + ); +} + +function RiskSettingsRoute() { + usePageTitle('Risk Settings · Site Audit'); + return ; +} + +function DocsRoute() { + usePageTitle('Docs · Site Audit'); + return ; +} + +export default function AppRoutes() { + return ( + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + } /> + + } /> + + }> + } /> + + + } /> + + ); +} diff --git a/web/app/chunk-load-recovery.tsx b/web/src/ChunkLoadRecovery.tsx similarity index 98% rename from web/app/chunk-load-recovery.tsx rename to web/src/ChunkLoadRecovery.tsx index 56db3fb9..811eb3d7 100644 --- a/web/app/chunk-load-recovery.tsx +++ b/web/src/ChunkLoadRecovery.tsx @@ -1,5 +1,3 @@ -'use client'; - import { useEffect } from 'react'; const RELOAD_KEY = 'wp-chunk-reload-once'; diff --git a/web/app/client-providers.tsx b/web/src/ClientProviders.tsx similarity index 98% rename from web/app/client-providers.tsx rename to web/src/ClientProviders.tsx index 3586437d..0f49b196 100644 --- a/web/app/client-providers.tsx +++ b/web/src/ClientProviders.tsx @@ -1,5 +1,3 @@ -'use client'; - import { Suspense, type ReactNode } from 'react'; import '@/patchConsole'; import { ThemeProvider } from '@/context/ThemeProvider'; diff --git a/web/src/ReportShell.tsx b/web/src/ReportShell.tsx index cc68f01e..2bf6c2f3 100644 --- a/web/src/ReportShell.tsx +++ b/web/src/ReportShell.tsx @@ -1,9 +1,5 @@ -'use client'; - -import { useState, useEffect, type ComponentType, type ReactNode } from 'react'; -import dynamic from 'next/dynamic'; -import Link from 'next/link'; -import { useRouter, usePathname, useSearchParams } from 'next/navigation'; +import { Suspense, useState, useEffect, type ComponentType, type ReactNode, lazy } from 'react'; +import { Link, useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import { Home as HomeIcon, LayoutDashboard, @@ -57,38 +53,35 @@ function viewLoading(label = 'Loading view…') { ); } -const Home = dynamic(() => import('./views/Home'), { loading: () => viewLoading() }); -const Overview = dynamic(() => import('./views/Overview'), { loading: () => viewLoading() }); -const Dashboards = dynamic(() => import('./views/Dashboards'), { loading: () => viewLoading('Loading dashboards…') }); -const CompareReports = dynamic(() => import('./views/CompareReports'), { loading: () => viewLoading() }); -const Issues = dynamic(() => import('./views/Issues'), { loading: () => viewLoading() }); -const Links = dynamic(() => import('./views/Links'), { loading: () => viewLoading() }); -const SiteStructure = dynamic(() => import('./views/SiteStructure'), { loading: () => viewLoading() }); -const Redirects = dynamic(() => import('./views/Redirects'), { loading: () => viewLoading() }); -const Content = dynamic(() => import('./views/Content'), { loading: () => viewLoading() }); -const Security = dynamic(() => import('./views/Security'), { loading: () => viewLoading() }); -const JavaScriptErrors = dynamic(() => import('./views/JavaScriptErrors'), { loading: () => viewLoading() }); -const AccessibilityView = dynamic(() => import('./views/Accessibility'), { loading: () => viewLoading() }); -const ImageSeo = dynamic(() => import('./views/ImageSeo'), { loading: () => viewLoading() }); -const GeoReadiness = dynamic(() => import('./views/GeoReadiness'), { loading: () => viewLoading() }); -const Lighthouse = dynamic(() => import('./views/Lighthouse'), { loading: () => viewLoading() }); -const Network = dynamic(() => import('./views/Network'), { - ssr: false, - loading: () => viewLoading('Loading network graph…'), -}); -const ContentAnalytics = dynamic(() => import('./views/ContentAnalytics'), { loading: () => viewLoading() }); -const TextContentAnalysis = dynamic(() => import('./views/TextContentAnalysis'), { loading: () => viewLoading() }); -const TechStack = dynamic(() => import('./views/TechStack'), { loading: () => viewLoading() }); -const Gallery = dynamic(() => import('./views/Gallery'), { loading: () => viewLoading() }); -const SearchPerformance = dynamic(() => import('./views/SearchPerformance'), { loading: () => viewLoading() }); -const Indexation = dynamic(() => import('./views/Indexation'), { loading: () => viewLoading() }); -const Backlinks = dynamic(() => import('./views/Backlinks'), { loading: () => viewLoading() }); -const Traffic = dynamic(() => import('./views/Traffic'), { loading: () => viewLoading() }); -const KeywordsExplorer = dynamic(() => import('./views/KeywordsExplorer'), { loading: () => viewLoading() }); -const ExportReport = dynamic(() => import('./views/ExportReport'), { loading: () => viewLoading() }); -const LogAnalyzer = dynamic(() => import('./views/LogAnalyzer'), { loading: () => viewLoading() }); -const Subdomains = dynamic(() => import('./views/Subdomains'), { loading: () => viewLoading() }); -const Contacts = dynamic(() => import('./views/Contacts'), { loading: () => viewLoading() }); +const Home = lazy(() => import('./views/Home')); +const Overview = lazy(() => import('./views/Overview')); +const Dashboards = lazy(() => import('./views/Dashboards')); +const CompareReports = lazy(() => import('./views/CompareReports')); +const Issues = lazy(() => import('./views/Issues')); +const Links = lazy(() => import('./views/Links')); +const SiteStructure = lazy(() => import('./views/SiteStructure')); +const Redirects = lazy(() => import('./views/Redirects')); +const Content = lazy(() => import('./views/Content')); +const Security = lazy(() => import('./views/Security')); +const JavaScriptErrors = lazy(() => import('./views/JavaScriptErrors')); +const AccessibilityView = lazy(() => import('./views/Accessibility')); +const ImageSeo = lazy(() => import('./views/ImageSeo')); +const GeoReadiness = lazy(() => import('./views/GeoReadiness')); +const Lighthouse = lazy(() => import('./views/Lighthouse')); +const Network = lazy(() => import('./views/Network')); +const ContentAnalytics = lazy(() => import('./views/ContentAnalytics')); +const TextContentAnalysis = lazy(() => import('./views/TextContentAnalysis')); +const TechStack = lazy(() => import('./views/TechStack')); +const Gallery = lazy(() => import('./views/Gallery')); +const SearchPerformance = lazy(() => import('./views/SearchPerformance')); +const Indexation = lazy(() => import('./views/Indexation')); +const Backlinks = lazy(() => import('./views/Backlinks')); +const Traffic = lazy(() => import('./views/Traffic')); +const KeywordsExplorer = lazy(() => import('./views/KeywordsExplorer')); +const ExportReport = lazy(() => import('./views/ExportReport')); +const LogAnalyzer = lazy(() => import('./views/LogAnalyzer')); +const Subdomains = lazy(() => import('./views/Subdomains')); +const Contacts = lazy(() => import('./views/Contacts')); interface ReportShellReportContext { data: ReportPayload | null; @@ -151,7 +144,7 @@ const VIEW_CONFIG: ViewConfigEntry[] = [ { id: 'keywords-explorer', component: KeywordsExplorer as ComponentType, icon: Key }, ]; -if (process.env.NODE_ENV !== 'production') { +if (import.meta.env.DEV) { const configIds = new Set(VIEW_CONFIG.map((entry) => entry.id)); for (const id of REPORT_VIEW_IDS) { if (!configIds.has(id)) { @@ -174,9 +167,9 @@ const VIEWS = VIEW_CONFIG.map((v) => ({ /** Sync `?domain=` query param with the active report payload. */ function BrandUrlSync({ slug }: SlugProps): null { const { data, loading, error, startUrlByRunId } = useReport() as ReportShellReportContext; - const searchParams = useSearchParams(); - const router = useRouter(); - const pathname = usePathname(); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const { pathname } = useLocation(); const searchStr = searchParams.toString(); useEffect(() => { @@ -186,8 +179,8 @@ function BrandUrlSync({ slug }: SlugProps): null { next.delete('domain'); next.delete('brand'); const q = next.toString(); - router.replace(q ? `${pathname}?${q}` : pathname); - }, [slug, searchStr, searchParams, router, pathname]); + navigate(q ? `${pathname}?${q}` : pathname, { replace: true }); + }, [slug, searchStr, searchParams, navigate, pathname]); useEffect(() => { if (slug === 'home') return; @@ -200,16 +193,16 @@ function BrandUrlSync({ slug }: SlugProps): null { const next = new URLSearchParams(searchStr); next.set('domain', value); const q = next.toString(); - router.replace(q ? `${pathname}?${q}` : pathname); - }, [slug, loading, error, data, searchStr, searchParams, router, pathname, startUrlByRunId]); + navigate(q ? `${pathname}?${q}` : pathname, { replace: true }); + }, [slug, loading, error, data, searchStr, searchParams, navigate, pathname, startUrlByRunId]); return null; } /** Main report shell layout and navigation. */ function AppContent({ slug }: SlugProps): ReactNode { - const router = useRouter(); - const searchParams = useSearchParams(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); const [searchQuery, setSearchQuery] = useState(''); const { loading, error, data, setSelectedReportId } = useReport() as ReportShellReportContext; @@ -221,18 +214,18 @@ function AppContent({ slug }: SlugProps): ReactNode { } const path = `/${viewIdToPathSlug(id)}`; if (id === 'home') { - router.push('/home'); + navigate('/home'); return; } if (opts?.domain != null && opts.domain !== '') { const p = new URLSearchParams(searchParams.toString()); p.set('domain', opts.domain); const q = p.toString(); - router.push(q ? `${path}?${q}` : path); + navigate(q ? `${path}?${q}` : path); return; } const q = searchParams.toString(); - router.push(q ? `${path}?${q}` : path); + navigate(q ? `${path}?${q}` : path); }; if (!view) { @@ -261,7 +254,7 @@ function AppContent({ slug }: SlugProps): ReactNode {

{strings.app.failedHint}

) : null} {strings.app.openRunAudit} @@ -282,18 +275,22 @@ function AppContent({ slug }: SlugProps): ReactNode { > {view === 'home' ? ( + + + + + ) : ( + - - ) : ( - + )} ); @@ -311,7 +308,7 @@ function RoutedShell({ slug }: SlugProps): ReactNode { /** Wraps children with ReportProvider (db + domain from URL). */ export function ReportAppClient({ children }: { children: ReactNode }): ReactNode { - const searchParams = useSearchParams(); + const [searchParams] = useSearchParams(); const domainRaw = searchParams.get('domain') ?? searchParams.get('brand'); const domainSlug = domainRaw != null && domainRaw !== '' ? domainRaw : null; diff --git a/web/src/app/api/report/compare/route.ts b/web/src/app/api/report/compare/route.ts deleted file mode 100644 index ac3cc336..00000000 --- a/web/src/app/api/report/compare/route.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { NextResponse, type NextRequest } from 'next/server'; -import { buildReportCompareResponse } from '@/server/reportCompareServer'; -import type { ApiRouteHandler } from '@/types/api'; - -export const dynamic = 'force-dynamic'; - -export const GET: ApiRouteHandler = async (request: NextRequest): Promise => { - const reportIdRaw = request.nextUrl.searchParams.get('reportId'); - const baselineIdRaw = request.nextUrl.searchParams.get('baselineId'); - - const reportId = reportIdRaw != null && reportIdRaw !== '' ? Number(reportIdRaw) : NaN; - const baselineId = baselineIdRaw != null && baselineIdRaw !== '' ? Number(baselineIdRaw) : NaN; - - if (!Number.isFinite(reportId) || !Number.isFinite(baselineId)) { - return NextResponse.json( - { error: 'reportId and baselineId query parameters are required' }, - { status: 400 }, - ); - } - - try { - const body = await buildReportCompareResponse(reportId, baselineId); - return NextResponse.json(body); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - const status = msg === 'Report not found' ? 404 : 500; - return NextResponse.json({ error: msg }, { status }); - } -}; diff --git a/web/src/components/AlertBanner.tsx b/web/src/components/AlertBanner.tsx index da5ee22b..54e8b81c 100644 --- a/web/src/components/AlertBanner.tsx +++ b/web/src/components/AlertBanner.tsx @@ -1,4 +1,3 @@ -'use client'; import { useState, type ReactNode } from 'react'; import { AlertTriangle, Info, XCircle, CheckCircle, X, ChevronRight, ChevronDown } from 'lucide-react'; diff --git a/web/src/components/AppLogo.tsx b/web/src/components/AppLogo.tsx index 8966a843..9b1d333d 100644 --- a/web/src/components/AppLogo.tsx +++ b/web/src/components/AppLogo.tsx @@ -1,4 +1,3 @@ -'use client'; import { useBranding } from '@/context/useBranding'; diff --git a/web/src/components/AppShell.tsx b/web/src/components/AppShell.tsx index ad0b6bfd..e89cfdcb 100644 --- a/web/src/components/AppShell.tsx +++ b/web/src/components/AppShell.tsx @@ -1,8 +1,7 @@ -'use client'; import { useEffect, useState, type ReactNode } from 'react'; -import Link from 'next/link'; -import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import { Link } from 'react-router-dom'; +import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import { ChevronLeft, ChevronRight, @@ -13,7 +12,7 @@ import { } from 'lucide-react'; import AppLogo from '@/components/AppLogo'; import IntegrationsModal from '@/components/IntegrationsModal'; -import { Badge, Breadcrumb, ReportSelector } from '@/components'; +import { Badge, ReportSelector } from '@/components'; import { useReport } from '@/context/useReport'; import { useSession } from '@/context/SessionContext'; import { useBranding } from '@/context/useBranding'; @@ -74,9 +73,9 @@ export default function AppShell({ onSearchChange, headerExtra, }: AppShellProps) { - const router = useRouter(); - const pathname = usePathname(); - const searchParams = useSearchParams(); + const navigate = useNavigate(); + const { pathname } = useLocation(); + const [searchParams] = useSearchParams(); const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [integrationsOpen, setIntegrationsOpen] = useState(false); @@ -130,7 +129,7 @@ export default function AppShell({ next.delete('auth'); next.delete('reason'); const q = next.toString(); - router.replace(q ? `${pathname}?${q}` : pathname); + navigate(q ? `${pathname}?${q}` : pathname, { replace: true }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -167,8 +166,6 @@ export default function AppShell({ ? format(strings.app.crawlCompletedSeconds, { seconds: crawlSummary.crawl_time_s }) : strings.app.crawlCompleted; - const activeNavItem = APP_NAV_ITEMS.find((item) => isNavItemActive(item, pathname)); - return (
{showSidebar && sidebarOpen ? ( @@ -194,7 +191,7 @@ export default function AppShell({ }`} > : }
- {activeNavItem ? ( - - ) : null} {showSearch && onSearchChange ? (
diff --git a/web/src/components/Breadcrumb.tsx b/web/src/components/Breadcrumb.tsx deleted file mode 100644 index 17348de1..00000000 --- a/web/src/components/Breadcrumb.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import Link from 'next/link'; -import { ChevronRight, type LucideIcon } from 'lucide-react'; - -export interface BreadcrumbItem { - label: string; - href?: string; - icon?: LucideIcon; -} - -export interface BreadcrumbProps { - items: BreadcrumbItem[]; - className?: string; -} - -/** - * Compact breadcrumb trail. The last item is treated as the current page. - * Shared by the global app header and the pipeline header for a consistent - * "you are here" affordance. - */ -export default function Breadcrumb({ items, className = '' }: BreadcrumbProps) { - return ( - - ); -} diff --git a/web/src/components/ChartCard.tsx b/web/src/components/ChartCard.tsx index 9b25c131..31613ff3 100644 --- a/web/src/components/ChartCard.tsx +++ b/web/src/components/ChartCard.tsx @@ -1,4 +1,3 @@ -'use client'; import type { ReactNode } from 'react'; import HelpHint, { normalizeHintContent, type HelpHintContent } from './HelpHint'; diff --git a/web/src/components/CountUp.tsx b/web/src/components/CountUp.tsx index 5d3fb572..8c544260 100644 --- a/web/src/components/CountUp.tsx +++ b/web/src/components/CountUp.tsx @@ -1,4 +1,3 @@ -'use client'; import { useCountUp } from '@/hooks/useCountUp'; diff --git a/web/src/components/CrawlScopeBanner.tsx b/web/src/components/CrawlScopeBanner.tsx index 1ca15924..4f5a683c 100644 --- a/web/src/components/CrawlScopeBanner.tsx +++ b/web/src/components/CrawlScopeBanner.tsx @@ -1,5 +1,5 @@ -import Link from 'next/link'; -import { useSearchParams } from 'next/navigation'; +import { Link } from 'react-router-dom'; +import { useSearchParams } from 'react-router-dom'; import { AlertTriangle } from 'lucide-react'; import AlertBanner from '@/components/AlertBanner'; import { strings, format } from '@/lib/strings'; @@ -8,7 +8,7 @@ import { javascriptErrorsViewHref } from '@/lib/browserErrors'; export default function CrawlScopeBanner({ data }: { data: ReportPayload | null | undefined }) { const cs = strings.views.overview.crawlScope; - const searchParams = useSearchParams(); + const [searchParams] = useSearchParams(); const trailingQuery = searchParams.toString(); const scope = (data?.report_meta as { crawl_scope?: Record } | undefined)?.crawl_scope; if (!scope) return null; @@ -97,7 +97,7 @@ export default function CrawlScopeBanner({ data }: { data: ReportPayload | null {showJsErrorsLink ? (

{cs.viewJavaScriptErrors} diff --git a/web/src/components/EmptyState.tsx b/web/src/components/EmptyState.tsx index 771dff3f..3744cfbd 100644 --- a/web/src/components/EmptyState.tsx +++ b/web/src/components/EmptyState.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react'; -import Link from 'next/link'; +import { Link } from 'react-router-dom'; import type { LucideIcon } from 'lucide-react'; import Button from './Button'; @@ -45,7 +45,7 @@ function ActionButton({ {action.label} ); - return action.href ? {btn} : btn; + return action.href ? {btn} : btn; } /** diff --git a/web/src/components/GoogleIntegrationsPanel.tsx b/web/src/components/GoogleIntegrationsPanel.tsx index 1cdc4009..a5d6896b 100644 --- a/web/src/components/GoogleIntegrationsPanel.tsx +++ b/web/src/components/GoogleIntegrationsPanel.tsx @@ -1,7 +1,6 @@ -'use client'; import { useState, useEffect, useCallback, useRef, useMemo, type ReactNode } from 'react'; -import Link from 'next/link'; +import { Link } from 'react-router-dom'; import { CheckCircle2, AlertCircle, @@ -15,7 +14,7 @@ import { Settings2, } from 'lucide-react'; import type { GooglePropertiesResponse, GoogleStatusResponse, IntegrationToast } from '@/types/api'; -import { apiUrl } from '@/lib/publicBase'; +import { apiUrl, apiFetch } from '@/lib/publicBase'; import { strings, format } from '@/lib/strings'; import { dispatchPipelineJobStarted, pollPipelineJob } from '@/lib/pipelineJobEvents'; import { useOptionalReport } from '@/context/useReport'; @@ -279,7 +278,7 @@ export default function GoogleIntegrationsPanel({ void (async () => { setLoadingPropertyRows(true); try { - const res = await fetch(apiUrl('/properties')); + const res = await apiFetch(apiUrl('/properties')); if (!res.ok) return; const data = (await res.json()) as { properties?: PropertyListItem[] }; if (!cancelled) setPropertyRows(data.properties ?? []); @@ -394,7 +393,7 @@ export default function GoogleIntegrationsPanel({ const url = startUrl.trim(); if (!url) return null; try { - const res = await fetch(apiUrl(`/properties/resolve?startUrl=${encodeURIComponent(url)}`)); + const res = await apiFetch(apiUrl(`/properties/resolve?startUrl=${encodeURIComponent(url)}`)); if (!res.ok) return null; const data = (await res.json()) as { id?: number }; if (data.id == null || !Number.isFinite(data.id)) return null; @@ -409,7 +408,7 @@ export default function GoogleIntegrationsPanel({ if (effectivePropertyId == null) return; setLoadingStatus(true); try { - const res = await fetch(endpoints.status); + const res = await apiFetch(endpoints.status); if (res.ok) { const data = (await res.json()) as GoogleStatusResponse & { connected?: boolean; @@ -461,7 +460,7 @@ export default function GoogleIntegrationsPanel({ } setLoadingLinksStatus(true); try { - const res = await fetch(endpoints.linksStatus); + const res = await apiFetch(endpoints.linksStatus); if (res.ok) { const data = (await res.json()) as typeof linksStatus; if (!isCancelled?.()) setLinksStatus(data); @@ -488,7 +487,7 @@ export default function GoogleIntegrationsPanel({ setLinksUploadMessage(''); try { const fileContent = await file.text(); - const res = await fetch(endpoints.linksImport, { + const res = await apiFetch(endpoints.linksImport, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fileContent, fileName: file.name }), @@ -530,7 +529,7 @@ export default function GoogleIntegrationsPanel({ const loadGoogleLists = async () => { setLoadingGoogleLists(true); try { - const res = await fetch(endpoints.listProperties); + const res = await apiFetch(endpoints.listProperties); if (res.ok) { const data = (await res.json()) as GooglePropertiesResponse; setGoogleLists(data); @@ -564,7 +563,7 @@ export default function GoogleIntegrationsPanel({ setSavingProps(true); setPropertiesSaveState({ phase: 'saving' }); try { - const res = await fetch(endpoints.credentials, { + const res = await apiFetch(endpoints.credentials, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -641,7 +640,7 @@ export default function GoogleIntegrationsPanel({ if (readOnly || !refreshToken.trim() || effectivePropertyId == null) return; setSavingToken(true); try { - const res = await fetch(endpoints.credentials, { + const res = await apiFetch(endpoints.credentials, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refreshToken: refreshToken.trim() }), @@ -666,7 +665,7 @@ export default function GoogleIntegrationsPanel({ setTesting(true); setTestLog(''); try { - const res = await fetch(endpoints.test, { method: 'POST' }); + const res = await apiFetch(endpoints.test, { method: 'POST' }); const data = await res.json(); const log = data.log || (data.ok ? 'Test passed.' : 'Test failed.'); setTestLog(log); @@ -705,7 +704,7 @@ export default function GoogleIntegrationsPanel({ fetchPollStopRef.current?.(); fetchPollStopRef.current = null; try { - const res = await fetch(apiUrl('/run'), { + const res = await apiFetch(apiUrl('/run'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -753,7 +752,7 @@ export default function GoogleIntegrationsPanel({ const handleDisconnect = async () => { if (readOnly) return; try { - await fetch(endpoints.disconnect, { method: 'POST' }); + await apiFetch(endpoints.disconnect, { method: 'POST' }); await fetchStatus(); setToast({ type: 'success', message: 'Disconnected.' }); } catch (e) { @@ -984,7 +983,7 @@ export default function GoogleIntegrationsPanel({

Need a project?{' '} {strings.docs.setupGuideLink} @@ -996,7 +995,7 @@ export default function GoogleIntegrationsPanel({

{strings.secrets.googleCredentialsHint}{' '} - + {strings.secrets.pageTitle}

@@ -1404,7 +1403,7 @@ export default function GoogleIntegrationsPanel({ {infoBannerText}

{strings.docs.setupGuideLink} diff --git a/web/src/components/HealthSparkline.tsx b/web/src/components/HealthSparkline.tsx index 2b50be13..5639d602 100644 --- a/web/src/components/HealthSparkline.tsx +++ b/web/src/components/HealthSparkline.tsx @@ -1,4 +1,3 @@ -'use client'; import Sparkline from '@/components/Sparkline'; diff --git a/web/src/components/HelpHint.tsx b/web/src/components/HelpHint.tsx index 61c01a50..a5ea6c37 100644 --- a/web/src/components/HelpHint.tsx +++ b/web/src/components/HelpHint.tsx @@ -1,4 +1,3 @@ -'use client'; import { useCallback, diff --git a/web/src/components/IntegrationsModal.tsx b/web/src/components/IntegrationsModal.tsx index 8410c086..5e8bbff9 100644 --- a/web/src/components/IntegrationsModal.tsx +++ b/web/src/components/IntegrationsModal.tsx @@ -1,4 +1,3 @@ -'use client'; import { Settings2, X } from 'lucide-react'; import type { IntegrationToast } from '@/types/api'; diff --git a/web/src/components/LandingShell.tsx b/web/src/components/LandingShell.tsx index 0d0e31e8..8c7ab2e2 100644 --- a/web/src/components/LandingShell.tsx +++ b/web/src/components/LandingShell.tsx @@ -1,4 +1,3 @@ -'use client'; import { useEffect, useRef, type ReactNode } from 'react'; import LandingDeckControls from '@/components/landing/LandingDeckControls'; diff --git a/web/src/components/ReportShellSkeleton.tsx b/web/src/components/ReportShellSkeleton.tsx index 0b8ed864..4c840d0d 100644 --- a/web/src/components/ReportShellSkeleton.tsx +++ b/web/src/components/ReportShellSkeleton.tsx @@ -1,4 +1,3 @@ -'use client'; import { Menu } from 'lucide-react'; import AppLogo from './AppLogo'; diff --git a/web/src/components/Reveal.tsx b/web/src/components/Reveal.tsx index baed7196..4c631b29 100644 --- a/web/src/components/Reveal.tsx +++ b/web/src/components/Reveal.tsx @@ -1,4 +1,3 @@ -'use client'; import { createElement, type ElementType, type ReactNode } from 'react'; import { useInView } from '@/lib/useInView'; diff --git a/web/src/components/SectionLoadingGate.tsx b/web/src/components/SectionLoadingGate.tsx index 2f72c948..2bae7ba1 100644 --- a/web/src/components/SectionLoadingGate.tsx +++ b/web/src/components/SectionLoadingGate.tsx @@ -1,4 +1,3 @@ -'use client'; import type { ReactNode } from 'react'; import type { SectionKey } from '@/lib/reportSections'; diff --git a/web/src/components/Sparkline.tsx b/web/src/components/Sparkline.tsx index d1889789..fa178e4e 100644 --- a/web/src/components/Sparkline.tsx +++ b/web/src/components/Sparkline.tsx @@ -1,4 +1,3 @@ -'use client'; export type SparklineMode = 'higher-better' | 'lower-better'; diff --git a/web/src/components/StatCard.tsx b/web/src/components/StatCard.tsx index 38ec37bb..7033d7c2 100644 --- a/web/src/components/StatCard.tsx +++ b/web/src/components/StatCard.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react'; -import Link from 'next/link'; +import { Link } from 'react-router-dom'; import HelpHint, { normalizeHintContent, type HelpHintContent } from './HelpHint'; import Card from './Card'; @@ -86,7 +86,7 @@ export default function StatCard({ if (href) { return ( {card} diff --git a/web/src/components/UrlInspectorButton.tsx b/web/src/components/UrlInspectorButton.tsx index f3a9e806..5b12e98b 100644 --- a/web/src/components/UrlInspectorButton.tsx +++ b/web/src/components/UrlInspectorButton.tsx @@ -1,4 +1,3 @@ -'use client'; import { Search } from 'lucide-react'; import { useOptionalUrlInspector } from '@/context/UrlInspectorContext'; diff --git a/web/src/components/UrlInspectorDrawer.tsx b/web/src/components/UrlInspectorDrawer.tsx index f4dbebb8..09ebc121 100644 --- a/web/src/components/UrlInspectorDrawer.tsx +++ b/web/src/components/UrlInspectorDrawer.tsx @@ -1,4 +1,3 @@ -'use client'; import { Fragment, useMemo } from 'react'; import { ChevronLeft, ChevronRight, X } from 'lucide-react'; diff --git a/web/src/components/ViewSectionLoader.tsx b/web/src/components/ViewSectionLoader.tsx index ae534e92..c0b4d474 100644 --- a/web/src/components/ViewSectionLoader.tsx +++ b/web/src/components/ViewSectionLoader.tsx @@ -1,4 +1,3 @@ -'use client'; import { pathSlugToViewId } from '@/routes'; import { useViewSections } from '@/hooks/useViewSections'; diff --git a/web/src/components/ViewSectionLoading.tsx b/web/src/components/ViewSectionLoading.tsx index 36d41bf7..bf710004 100644 --- a/web/src/components/ViewSectionLoading.tsx +++ b/web/src/components/ViewSectionLoading.tsx @@ -1,4 +1,3 @@ -'use client'; import { PageLayout, PageHeader } from '@/components'; import { Skeleton } from '@/components/Skeleton'; diff --git a/web/src/components/ViewTabs.tsx b/web/src/components/ViewTabs.tsx index ab21f3c5..2346487b 100644 --- a/web/src/components/ViewTabs.tsx +++ b/web/src/components/ViewTabs.tsx @@ -1,4 +1,3 @@ -'use client'; import type { ReactNode } from 'react'; diff --git a/web/src/components/ai/AiSuggestionButton.tsx b/web/src/components/ai/AiSuggestionButton.tsx index c7419196..549ccbce 100644 --- a/web/src/components/ai/AiSuggestionButton.tsx +++ b/web/src/components/ai/AiSuggestionButton.tsx @@ -1,8 +1,7 @@ -'use client'; import { useState, useCallback } from 'react'; import { Loader2, Sparkles } from 'lucide-react'; -import { apiUrl } from '@/lib/publicBase'; +import { apiUrl, apiFetch } from '@/lib/publicBase'; import { strings } from '@/lib/strings'; import { useReadOnlySession } from '@/hooks/useReadOnlySession'; import CopyBtn from '@/components/links/CopyBtn'; @@ -29,7 +28,7 @@ export default function AiSuggestionButton({ request, initialText = null, classN setLoading(true); setError(null); try { - const res = await fetch(apiUrl('/ai/fix-suggestion'), { + const res = await apiFetch(apiUrl('/ai/fix-suggestion'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/web/src/components/backlinks/CompetitorGapImport.tsx b/web/src/components/backlinks/CompetitorGapImport.tsx index f9080673..aca29673 100644 --- a/web/src/components/backlinks/CompetitorGapImport.tsx +++ b/web/src/components/backlinks/CompetitorGapImport.tsx @@ -1,8 +1,7 @@ -'use client'; import { useRef, useState, useCallback } from 'react'; import { Upload, Loader2 } from 'lucide-react'; -import { apiUrl } from '@/lib/publicBase'; +import { apiUrl, apiFetch } from '@/lib/publicBase'; import { strings, format } from '@/lib/strings'; import { Button } from '@/components'; import { useReadOnlySession } from '@/hooks/useReadOnlySession'; @@ -50,7 +49,7 @@ export default function CompetitorGapImport({ gscLinks }: CompetitorGapImportPro setGap(null); try { const csvText = await file.text(); - const res = await fetch(apiUrl('/backlinks/competitor-import'), { + const res = await apiFetch(apiUrl('/backlinks/competitor-import'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ competitor: comp, csvText, ourDomains }), diff --git a/web/src/components/backlinks/ThirdPartyLinksImport.tsx b/web/src/components/backlinks/ThirdPartyLinksImport.tsx index db6c4176..50b9c72f 100644 --- a/web/src/components/backlinks/ThirdPartyLinksImport.tsx +++ b/web/src/components/backlinks/ThirdPartyLinksImport.tsx @@ -1,8 +1,7 @@ -'use client'; import { useRef, useState, useCallback } from 'react'; import { Upload, Loader2 } from 'lucide-react'; -import { apiUrl } from '@/lib/publicBase'; +import { apiUrl, apiFetch } from '@/lib/publicBase'; import { strings, format } from '@/lib/strings'; import { Button } from '@/components'; import { useReadOnlySession } from '@/hooks/useReadOnlySession'; @@ -55,7 +54,7 @@ export default function ThirdPartyLinksImport({ gscLinks, onImported }: ThirdPar setError(null); try { const csvText = await file.text(); - const res = await fetch(apiUrl('/backlinks/third-party-import'), { + const res = await apiFetch(apiUrl('/backlinks/third-party-import'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ propertyId, provider, csvText, ourDomains }), diff --git a/web/src/components/charts/CategoryScoreGauge.tsx b/web/src/components/charts/CategoryScoreGauge.tsx index cddae341..452a0a1e 100644 --- a/web/src/components/charts/CategoryScoreGauge.tsx +++ b/web/src/components/charts/CategoryScoreGauge.tsx @@ -1,4 +1,3 @@ -'use client'; import { format, strings } from '@/lib/strings'; import { scoreBandColor } from '@/utils/chartPalette'; diff --git a/web/src/components/charts/DistributionChart.tsx b/web/src/components/charts/DistributionChart.tsx index d1c36a40..807a5546 100644 --- a/web/src/components/charts/DistributionChart.tsx +++ b/web/src/components/charts/DistributionChart.tsx @@ -1,4 +1,3 @@ -'use client'; import type { StatusDistribution } from '@/lib/statusDistribution'; import { palette } from '@/utils/chartPalette'; diff --git a/web/src/components/charts/RankedBarChart.tsx b/web/src/components/charts/RankedBarChart.tsx index 7f74e388..acb79732 100644 --- a/web/src/components/charts/RankedBarChart.tsx +++ b/web/src/components/charts/RankedBarChart.tsx @@ -1,4 +1,3 @@ -'use client'; import { Bar } from 'react-chartjs-2'; import type { ChartData, ChartOptions } from 'chart.js'; diff --git a/web/src/components/charts/ScoreDelta.tsx b/web/src/components/charts/ScoreDelta.tsx index daec4424..20db7222 100644 --- a/web/src/components/charts/ScoreDelta.tsx +++ b/web/src/components/charts/ScoreDelta.tsx @@ -1,4 +1,3 @@ -'use client'; import { Minus, TrendingDown, TrendingUp } from 'lucide-react'; diff --git a/web/src/components/charts/SimpleBarChart.tsx b/web/src/components/charts/SimpleBarChart.tsx index fd434a21..3f3fb8af 100644 --- a/web/src/components/charts/SimpleBarChart.tsx +++ b/web/src/components/charts/SimpleBarChart.tsx @@ -1,4 +1,3 @@ -'use client'; import { useMemo } from 'react'; import { Bar } from 'react-chartjs-2'; diff --git a/web/src/components/charts/compact/CompactAreaSparkline.tsx b/web/src/components/charts/compact/CompactAreaSparkline.tsx index 1d22a9c5..dafc9e62 100644 --- a/web/src/components/charts/compact/CompactAreaSparkline.tsx +++ b/web/src/components/charts/compact/CompactAreaSparkline.tsx @@ -1,4 +1,3 @@ -'use client'; import { useId } from 'react'; import { scaleLinear } from 'd3-scale'; diff --git a/web/src/components/charts/compact/CompactBarChart.tsx b/web/src/components/charts/compact/CompactBarChart.tsx index edfd8ff8..3d54de84 100644 --- a/web/src/components/charts/compact/CompactBarChart.tsx +++ b/web/src/components/charts/compact/CompactBarChart.tsx @@ -1,4 +1,3 @@ -'use client'; import { useRef } from 'react'; import { scaleBand, scaleLinear } from 'd3-scale'; diff --git a/web/src/components/charts/compact/CompactDonut.tsx b/web/src/components/charts/compact/CompactDonut.tsx index 2f6ffb33..959ea5e5 100644 --- a/web/src/components/charts/compact/CompactDonut.tsx +++ b/web/src/components/charts/compact/CompactDonut.tsx @@ -1,4 +1,3 @@ -'use client'; import { arc, pie } from 'd3-shape'; import { scaleLinear } from 'd3-scale'; diff --git a/web/src/components/charts/d3/D3DonutChart.tsx b/web/src/components/charts/d3/D3DonutChart.tsx index 3edc9903..e2468cc2 100644 --- a/web/src/components/charts/d3/D3DonutChart.tsx +++ b/web/src/components/charts/d3/D3DonutChart.tsx @@ -1,4 +1,3 @@ -'use client'; import { useRef, useState } from 'react'; import { arc, pie } from 'd3-shape'; diff --git a/web/src/components/charts/d3/D3DualLineChart.tsx b/web/src/components/charts/d3/D3DualLineChart.tsx index bc0f6aa0..d66989b2 100644 --- a/web/src/components/charts/d3/D3DualLineChart.tsx +++ b/web/src/components/charts/d3/D3DualLineChart.tsx @@ -1,4 +1,3 @@ -'use client'; import { useRef, useState } from 'react'; import { scaleLinear, scalePoint } from 'd3-scale'; diff --git a/web/src/components/charts/d3/D3ForceGraph.tsx b/web/src/components/charts/d3/D3ForceGraph.tsx index ad02f116..cc32dcfe 100644 --- a/web/src/components/charts/d3/D3ForceGraph.tsx +++ b/web/src/components/charts/d3/D3ForceGraph.tsx @@ -1,4 +1,3 @@ -'use client'; import { useEffect, useRef } from 'react'; import { hierarchy, tree, select, zoom, type HierarchyNode, type HierarchyPointNode, type ZoomBehavior } from 'd3'; diff --git a/web/src/components/charts/d3/D3GroupedBarChart.tsx b/web/src/components/charts/d3/D3GroupedBarChart.tsx index bd6a2705..6352c055 100644 --- a/web/src/components/charts/d3/D3GroupedBarChart.tsx +++ b/web/src/components/charts/d3/D3GroupedBarChart.tsx @@ -1,4 +1,3 @@ -'use client'; import { useRef, useState } from 'react'; import { scaleBand, scaleLinear } from 'd3-scale'; diff --git a/web/src/components/charts/d3/D3HorizontalBarChart.tsx b/web/src/components/charts/d3/D3HorizontalBarChart.tsx index 4020a7eb..353f4c20 100644 --- a/web/src/components/charts/d3/D3HorizontalBarChart.tsx +++ b/web/src/components/charts/d3/D3HorizontalBarChart.tsx @@ -1,4 +1,3 @@ -'use client'; import { useRef, useState } from 'react'; import { scaleBand, scaleLinear } from 'd3-scale'; diff --git a/web/src/components/charts/d3/D3StackedHorizontalBarChart.tsx b/web/src/components/charts/d3/D3StackedHorizontalBarChart.tsx index cc3227c4..10704b34 100644 --- a/web/src/components/charts/d3/D3StackedHorizontalBarChart.tsx +++ b/web/src/components/charts/d3/D3StackedHorizontalBarChart.tsx @@ -1,4 +1,3 @@ -'use client'; import { useRef, useState } from 'react'; import { scaleBand, scaleLinear } from 'd3-scale'; diff --git a/web/src/components/charts/d3/D3StackedVerticalBarChart.tsx b/web/src/components/charts/d3/D3StackedVerticalBarChart.tsx index d4266378..ca0ccd1e 100644 --- a/web/src/components/charts/d3/D3StackedVerticalBarChart.tsx +++ b/web/src/components/charts/d3/D3StackedVerticalBarChart.tsx @@ -1,4 +1,3 @@ -'use client'; import { useRef, useState } from 'react'; import { scaleBand, scaleLinear } from 'd3-scale'; diff --git a/web/src/components/charts/d3/D3VerticalBarChart.tsx b/web/src/components/charts/d3/D3VerticalBarChart.tsx index b4b6e15f..c58da239 100644 --- a/web/src/components/charts/d3/D3VerticalBarChart.tsx +++ b/web/src/components/charts/d3/D3VerticalBarChart.tsx @@ -1,4 +1,3 @@ -'use client'; import { useRef, useState } from 'react'; import { scaleBand, scaleLinear } from 'd3-scale'; diff --git a/web/src/components/chat/ChatActivityBar.tsx b/web/src/components/chat/ChatActivityBar.tsx index 4800cc9b..e6a0712a 100644 --- a/web/src/components/chat/ChatActivityBar.tsx +++ b/web/src/components/chat/ChatActivityBar.tsx @@ -1,4 +1,3 @@ -'use client'; import { Loader2 } from 'lucide-react'; import { format, strings } from '@/lib/strings'; diff --git a/web/src/components/chat/ChatAssistantAvatar.tsx b/web/src/components/chat/ChatAssistantAvatar.tsx index 9f5328f9..61d267be 100644 --- a/web/src/components/chat/ChatAssistantAvatar.tsx +++ b/web/src/components/chat/ChatAssistantAvatar.tsx @@ -1,4 +1,3 @@ -'use client'; import { useState } from 'react'; import { Bot } from 'lucide-react'; diff --git a/web/src/components/chat/ChatAssistantMessage.tsx b/web/src/components/chat/ChatAssistantMessage.tsx index 690c4213..cb0f3791 100644 --- a/web/src/components/chat/ChatAssistantMessage.tsx +++ b/web/src/components/chat/ChatAssistantMessage.tsx @@ -1,4 +1,3 @@ -'use client'; import { Sparkles } from 'lucide-react'; import ChatBlocks from '@/components/chat/blocks/ChatBlocks'; diff --git a/web/src/components/chat/ChatComposer.tsx b/web/src/components/chat/ChatComposer.tsx index 07c6f237..e31bd80d 100644 --- a/web/src/components/chat/ChatComposer.tsx +++ b/web/src/components/chat/ChatComposer.tsx @@ -1,4 +1,3 @@ -'use client'; import { useCallback, useEffect, useRef, useState, type FormEvent, type ReactNode } from 'react'; import { Loader2, Plus, Send } from 'lucide-react'; diff --git a/web/src/components/chat/ChatContextBar.tsx b/web/src/components/chat/ChatContextBar.tsx index 809e9d1d..f40974dc 100644 --- a/web/src/components/chat/ChatContextBar.tsx +++ b/web/src/components/chat/ChatContextBar.tsx @@ -1,4 +1,3 @@ -'use client'; import { Globe } from 'lucide-react'; import { formatChatPropertyLabel } from '@/lib/chatPropertyLabel'; diff --git a/web/src/components/chat/ChatFab.tsx b/web/src/components/chat/ChatFab.tsx index 3c6bc1a6..dd27c2ac 100644 --- a/web/src/components/chat/ChatFab.tsx +++ b/web/src/components/chat/ChatFab.tsx @@ -1,7 +1,6 @@ -'use client'; import { MessageSquare, X } from 'lucide-react'; -import { usePathname, useSearchParams } from 'next/navigation'; +import { useLocation, useSearchParams } from 'react-router-dom'; import { useCallback, useEffect, useRef, useState, type CSSProperties, type PointerEvent as ReactPointerEvent } from 'react'; import { chatFabCornerStyle, @@ -23,8 +22,8 @@ const s = strings.components.chat; * Drag to any screen corner; position is remembered across sessions. */ export default function ChatFab() { - const pathname = usePathname(); - const searchParams = useSearchParams(); + const { pathname } = useLocation(); + const [searchParams] = useSearchParams(); const domain = searchParams.get('domain') ?? searchParams.get('brand'); const [open, setOpen] = useState(false); diff --git a/web/src/components/chat/ChatFabDrawer.tsx b/web/src/components/chat/ChatFabDrawer.tsx index 0dadb731..7f80f612 100644 --- a/web/src/components/chat/ChatFabDrawer.tsx +++ b/web/src/components/chat/ChatFabDrawer.tsx @@ -1,7 +1,6 @@ -'use client'; import { useEffect, useRef } from 'react'; -import Link from 'next/link'; +import { Link } from 'react-router-dom'; import { ArrowUpRight, ChevronDown, Loader2, Maximize2, RotateCcw } from 'lucide-react'; import { strings } from '@/lib/strings'; import ChatAssistantAvatar from '@/components/chat/ChatAssistantAvatar'; @@ -94,7 +93,7 @@ export default function ChatFabDrawer({ open, domain, onClose }: ChatFabDrawerPr )} diff --git a/web/src/components/chat/ChatFollowUpContext.tsx b/web/src/components/chat/ChatFollowUpContext.tsx index bfdde745..16b0969d 100644 --- a/web/src/components/chat/ChatFollowUpContext.tsx +++ b/web/src/components/chat/ChatFollowUpContext.tsx @@ -1,4 +1,3 @@ -'use client'; import { createContext, useContext, type ReactNode } from 'react'; diff --git a/web/src/components/chat/ChatInsightSections.tsx b/web/src/components/chat/ChatInsightSections.tsx index d4da2250..667e9356 100644 --- a/web/src/components/chat/ChatInsightSections.tsx +++ b/web/src/components/chat/ChatInsightSections.tsx @@ -1,4 +1,3 @@ -'use client'; import { useMemo, useState } from 'react'; import { ChevronDown, ChevronRight, Lightbulb } from 'lucide-react'; diff --git a/web/src/components/chat/ChatMarkdown.tsx b/web/src/components/chat/ChatMarkdown.tsx index e39fba69..2cf2690f 100644 --- a/web/src/components/chat/ChatMarkdown.tsx +++ b/web/src/components/chat/ChatMarkdown.tsx @@ -1,4 +1,3 @@ -'use client'; import { useMemo } from 'react'; import ReactMarkdown from 'react-markdown'; diff --git a/web/src/components/chat/ChatMessageList.tsx b/web/src/components/chat/ChatMessageList.tsx index cf7dd425..722b4dc8 100644 --- a/web/src/components/chat/ChatMessageList.tsx +++ b/web/src/components/chat/ChatMessageList.tsx @@ -1,4 +1,3 @@ -'use client'; import { useEffect, useRef } from 'react'; import ChatAssistantMessage from '@/components/chat/ChatAssistantMessage'; diff --git a/web/src/components/chat/ChatModelPicker.tsx b/web/src/components/chat/ChatModelPicker.tsx index aebff8eb..b29467cb 100644 --- a/web/src/components/chat/ChatModelPicker.tsx +++ b/web/src/components/chat/ChatModelPicker.tsx @@ -1,7 +1,6 @@ -'use client'; import { useEffect, useMemo, useRef, useState } from 'react'; -import Link from 'next/link'; +import { Link } from 'react-router-dom'; import { Check, ChevronDown, Circle, Loader2, RefreshCw } from 'lucide-react'; import { useOllamaModels, type OllamaModelEntry } from '@/hooks/useOllamaModels'; import { @@ -299,7 +298,7 @@ export default function ChatModelPicker({ )} setOpen(false)} > diff --git a/web/src/components/chat/ChatNarrativeSections.tsx b/web/src/components/chat/ChatNarrativeSections.tsx index 65c39272..11c2acd7 100644 --- a/web/src/components/chat/ChatNarrativeSections.tsx +++ b/web/src/components/chat/ChatNarrativeSections.tsx @@ -1,4 +1,3 @@ -'use client'; import { useState } from 'react'; import { ChevronDown, ChevronRight, Lightbulb, ListChecks } from 'lucide-react'; diff --git a/web/src/components/chat/ChatProviderPicker.tsx b/web/src/components/chat/ChatProviderPicker.tsx index 16ef5b7c..b07aa892 100644 --- a/web/src/components/chat/ChatProviderPicker.tsx +++ b/web/src/components/chat/ChatProviderPicker.tsx @@ -1,7 +1,6 @@ -'use client'; import { useEffect, useRef, useState } from 'react'; -import Link from 'next/link'; +import { Link } from 'react-router-dom'; import { Check, ChevronDown } from 'lucide-react'; import { LLM_PROVIDER_LABELS, LLM_CLOUD_PROVIDERS } from '@/lib/llmProviderApiKeys'; import { strings } from '@/lib/strings'; @@ -125,7 +124,7 @@ export default function ChatProviderPicker({ {saveError ?

{saveError}

: null}
setOpen(false)} > diff --git a/web/src/components/chat/ChatShell.tsx b/web/src/components/chat/ChatShell.tsx index 68b4bad3..515603f1 100644 --- a/web/src/components/chat/ChatShell.tsx +++ b/web/src/components/chat/ChatShell.tsx @@ -1,4 +1,3 @@ -'use client'; import { useCallback, useState, type ReactNode } from 'react'; diff --git a/web/src/components/chat/ChatSidebar.tsx b/web/src/components/chat/ChatSidebar.tsx index 32bb82a0..50d6b125 100644 --- a/web/src/components/chat/ChatSidebar.tsx +++ b/web/src/components/chat/ChatSidebar.tsx @@ -1,8 +1,7 @@ -'use client'; import { useEffect, useRef, useState, type ReactNode } from 'react'; -import Link from 'next/link'; -import { usePathname } from 'next/navigation'; +import { Link } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import { ChevronLeft, History, @@ -86,21 +85,21 @@ function SettingsMenu({ onClose }: { onClose: () => void }) {
{strings.settings.settingsLink} {c.aiSettingsLink} @@ -124,7 +123,7 @@ export default function ChatSidebar({ toggle, setExpanded, }: ChatSidebarProps) { - const pathname = usePathname(); + const { pathname } = useLocation(); const [settingsOpen, setSettingsOpen] = useState(false); const settingsRef = useRef(null); @@ -186,7 +185,7 @@ export default function ChatSidebar({ if (!expanded) { return (
- + @@ -231,7 +230,7 @@ export default function ChatSidebar({