diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 205e7b6..47f1e58 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -1,4 +1,4 @@ -name: Build and Publish Website Docker Image +name: Build and Publish Docker Images on: push: @@ -13,7 +13,19 @@ env: jobs: build: + name: Build ${{ matrix.service }} image runs-on: ubuntu-latest + strategy: + matrix: + include: + - service: website + file: ./website/Dockerfile + context: . + image_suffix: "" + - service: frontend + file: ./frontend/Dockerfile + context: ./frontend + image_suffix: "-frontend" permissions: contents: read packages: write @@ -38,7 +50,7 @@ jobs: id: meta uses: docker/metadata-action@v6 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.image_suffix }} tags: | type=ref,event=branch type=ref,event=pr @@ -51,10 +63,21 @@ jobs: id: build-and-push uses: docker/build-push-action@v7 with: - context: . - file: ./website.Dockerfile + context: ${{ matrix.context }} + file: ${{ matrix.file }} push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max + + deploy: + name: Deploy + runs-on: ubuntu-latest + needs: build + if: github.event_name != 'pull_request' + + steps: + - name: Trigger Coolify deploy + run: | + curl --fail --request GET '${{ secrets.COOLIFY_WEBHOOK }}' --header 'Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}' diff --git a/Cargo.lock b/Cargo.lock index bd235e5..994a82d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -751,7 +751,7 @@ dependencies = [ "atomic 0.6.1", "pear", "serde", - "toml 0.8.23", + "toml", "uncased", "version_check", ] @@ -831,15 +831,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "frontend" -version = "0.0.1" -dependencies = [ - "include_dir", - "serde", - "toml 0.9.12+spec-1.1.0", -] - [[package]] name = "fsevent-sys" version = "4.1.0" @@ -1591,25 +1582,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "include_dir" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" -dependencies = [ - "include_dir_macros", -] - -[[package]] -name = "include_dir_macros" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" -dependencies = [ - "proc-macro2", - "quote", -] - [[package]] name = "indexmap" version = "2.12.1" @@ -3226,15 +3198,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3926,26 +3889,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", + "serde_spanned", + "toml_datetime", "toml_edit", ] -[[package]] -name = "toml" -version = "0.9.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned 1.1.1", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 0.7.13", -] - [[package]] name = "toml_datetime" version = "0.6.11" @@ -3955,15 +3903,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - [[package]] name = "toml_edit" version = "0.22.27" @@ -3972,19 +3911,10 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", + "serde_spanned", + "toml_datetime", "toml_write", - "winnow 0.7.13", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow 1.0.3", + "winnow", ] [[package]] @@ -3993,12 +3923,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - [[package]] name = "tower" version = "0.5.2" @@ -4443,7 +4367,6 @@ dependencies = [ "dotenvy", "env_logger", "flate2", - "frontend", "governor", "log", "querying", @@ -4824,12 +4747,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winnow" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" - [[package]] name = "winreg" version = "0.50.0" diff --git a/Cargo.toml b/Cargo.toml index c3bccdf..5b10cee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,5 @@ [workspace] members = [ - "frontend", "querying", "reporter", "reports", diff --git a/README.md b/README.md index 3e9b17c..f0ee41a 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,81 @@ -# ![Cheburcheck Logo](website/static/favicon.svg) Cheburcheck.ru - -[Cheburcheck.ru](https://cheburcheck.ru) — open-source сервис для проверки доменов и IP-адресов на наличие в списках блокировок Роскомнадзора и популярных заблокированных CDN-провайдеров. - ---- - -## О проекте - -Этот сервис реализован на языке Rust с использованием веб-фреймворка [Rocket.rs](https://rocket.rs/). Основная задача — проверять, заблокирован ли домен или IP-адрес, основываясь на актуальных данных из реестров Роскомнадзора и других источниках. - ---- - -## Технологии - -- **Rust** — язык программирования, на котором написан весь бекенд. -- **Rocket.rs** — веб-фреймворк для построения HTTP API и серверной логики. -- **DoH резолвер Quad9** — для разрешения DNS-запросов используется DNS-over-HTTPS, что позволяет обойти локальные ограничения и получать актуальные данные. -- **Tera** — шаблонизатор для генерации HTML страниц, используется для фронтенда. -- **lucide** — библиотека иконок, применяемая для визуального оформления интерфейса. - ---- - -## Как это работает - -1. Пользователь вводит домен или IP-адрес в форму на сайте. -2. Сервер выполняет DNS-запрос через Quad9 DoH для получения актуальной информации. -3. Полученный домен/IP проверяется на наличие в блокировочных списках через префиксные деревья. -4. Результат формируется с помощью Tera и отображается пользователю. - ---- - -## Списки - -Для проверки используются списки [123jjck/cdn-ip-ranges](https://github.com/123jjck/cdn-ip-ranges), [antifilter.download](https://antifilter.download/) и [antifilter.network](https://antifilter.network). - -Мы собираем собственные белые списки с помощью [Cheburcheck Reporter](reporter/README.md). - ---- - -## Структура проекта - -* `querying` — модуль проверки сайтов по базам данных -* `reporter` — [Cheburcheck Reporter](reporter/README.md) +# ![Cheburcheck Logo](website/static/favicon.svg) Cheburcheck.ru + +[Cheburcheck.ru](https://cheburcheck.ru) — open-source сервис для проверки доменов и IP-адресов на наличие в списках блокировок Роскомнадзора и популярных заблокированных CDN-провайдеров. + +--- + +## О проекте + +Этот сервис реализован на языке Rust с использованием веб-фреймворка [Rocket.rs](https://rocket.rs/). Основная задача — проверять, заблокирован ли домен или IP-адрес, основываясь на актуальных данных из реестров Роскомнадзора и других источниках. + +--- + +## Технологии + +- **Rust** — язык программирования, на котором написан весь бекенд. +- **Rocket.rs** — веб-фреймворк для построения HTTP API и серверной логики. +- **DoH резолвер Quad9** — для разрешения DNS-запросов используется DNS-over-HTTPS, что позволяет обойти локальные ограничения и получать актуальные данные. +- **SvelteKit** — SSR-фронтенд, который запускается отдельным Node.js сервисом. +- **nginx** — публичная точка входа, проксирует API в Rocket, а страницы в SvelteKit. +- **lucide** — библиотека иконок, применяемая для визуального оформления интерфейса. + +--- + +## Как это работает + +1. Пользователь вводит домен или IP-адрес в форму на сайте. +2. Сервер выполняет DNS-запрос через Quad9 DoH для получения актуальной информации. +3. Полученный домен/IP проверяется на наличие в блокировочных списках через префиксные деревья. +4. Результат возвращается через API и отображается SSR-фронтендом SvelteKit. + +--- + +## Списки + +Для проверки используются списки [123jjck/cdn-ip-ranges](https://github.com/123jjck/cdn-ip-ranges), [antifilter.download](https://antifilter.download/) и [antifilter.network](https://antifilter.network). + +Мы собираем собственные белые списки с помощью [Cheburcheck Reporter](reporter/README.md). + +--- + +## Структура проекта + +* `querying` — модуль проверки сайтов по базам данных +* `reporter` — [Cheburcheck Reporter](reporter/README.md) * `reports` — общий протокол для отправки отчетов -* `website` — исходный код веб-сайта +* `website` — Rocket API и серверная логика +* `frontend` — SvelteKit SSR-интерфейс --- -## Вклад +## Запуск через Docker Compose -Если хотите помочь с разработкой — открывайте issue или присылайте pull requests. -Если хотите помочь финансово: -- TON: `UQAACsiwpGryjP-kqp4TJPAWpXytuB6M_puuO0Cg5zNvaSJW` +```sh +docker compose up --build +``` ---- +По умолчанию nginx слушает `http://localhost:8080`. Порт можно изменить через +`HTTP_PORT`, например: -## Контакты +```sh +HTTP_PORT=80 docker compose up --build +``` -Для вопросов и обсуждений можно написать на [support@cheburcheck.ru](mailto:support@cheburcheck.ru). +Маршрутизация: + +* `/api/v1/*`, `/agency/*`, `/whitelist/*` и устаревший `/feedback/*` идут в Rocket +* остальные запросы идут в SvelteKit SSR + +--- + +## Вклад + +Если хотите помочь с разработкой — открывайте issue или присылайте pull requests. +Если хотите помочь финансово: +- TON: `UQAACsiwpGryjP-kqp4TJPAWpXytuB6M_puuO0Cg5zNvaSJW` + +--- + +## Контакты + +Для вопросов и обсуждений можно написать на [support@cheburcheck.ru](mailto:support@cheburcheck.ru). diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..4a13c61 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,32 @@ +services: + nginx: + image: docker.io/nginx:1.27-alpine + depends_on: + - frontend + - website + ports: + - "${HTTP_PORT:-8080}:80" + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + environment: + HOST: 0.0.0.0 + PORT: 3000 + NODE_ENV: production + expose: + - "3000" + + website: + build: + context: . + dockerfile: website/Dockerfile + environment: + ROCKET_ADDRESS: "::" + ROCKET_PORT: 8000 + DATABASE_URL: "${DATABASE_URL}" + expose: + - "8000" diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..aeed687 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,10 @@ +node_modules +.svelte-kit +build +dist +.env +.env.* +!.env.example +!.env.test +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml deleted file mode 100644 index 604fd68..0000000 --- a/frontend/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "frontend" -version = "0.0.1" -edition = "2024" - -[dependencies] -include_dir = "0.7" - -[build-dependencies] -serde = { workspace = true } -toml = "0.9" diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..dae2ac3 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,43 @@ +# syntax=docker/dockerfile:1.10 + +FROM docker.io/node:24-bookworm-slim AS build + +WORKDIR /app + +ENV PNPM_HOME=/pnpm +ENV PATH="${PNPM_HOME}:${PATH}" + +RUN corepack enable && \ + corepack prepare pnpm@10.33.0 --activate && \ + pnpm config set store-dir /pnpm/store + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ + +RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \ + pnpm fetch --frozen-lockfile + +COPY . ./ + +RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \ + set -eux; \ + pnpm install --frozen-lockfile --offline; \ + pnpm build; \ + pnpm prune --prod + +FROM docker.io/node:24-bookworm-slim + +WORKDIR /app + +ENV HOST=0.0.0.0 +ENV PORT=3000 +ENV NODE_ENV=production + +COPY --from=build /app/build ./build +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/package.json ./package.json + +EXPOSE 3000 + +USER node + +CMD ["node", "build/index.js"] diff --git a/frontend/build.rs b/frontend/build.rs deleted file mode 100644 index 7baf761..0000000 --- a/frontend/build.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::error::Error; -use std::path::PathBuf; -use std::process::Command; - -fn main() -> Result<(), Box> { - println!("cargo:rerun-if-changed=package.json"); - println!("cargo:rerun-if-changed=pnpm-lock.yaml"); - println!("cargo:rerun-if-changed=svelte.config.js"); - println!("cargo:rerun-if-changed=vite.config.ts"); - println!("cargo:rerun-if-changed=src"); - println!("cargo:rerun-if-changed=static"); - println!("cargo:rerun-if-changed=../website/Cargo.toml"); - - if std::env::var_os("SKIP_FRONTEND_BUILD").is_some() { - return Ok(()); - } - - let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?); - - run_pnpm(&manifest_dir, &["install", "--frozen-lockfile"])?; - run_pnpm(&manifest_dir, &["build"])?; - - Ok(()) -} - -fn run_pnpm(manifest_dir: &PathBuf, args: &[&str]) -> Result<(), Box> { - let mut command = Command::new(pnpm()); - command.args(args).current_dir(manifest_dir); - - let status = command.status()?; - - if status.success() { - Ok(()) - } else { - Err(format!("pnpm {} failed", args.join(" ")).into()) - } -} - -fn pnpm() -> &'static str { - if cfg!(windows) { "pnpm.cmd" } else { "pnpm" } -} diff --git a/frontend/package.json b/frontend/package.json index 99533d9..a3de099 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,6 +7,7 @@ "dev": "vite dev", "build": "vite build", "preview": "vite preview", + "start": "node build/index.js", "prepare": "svelte-kit sync || echo ''", "type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "type-check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", @@ -16,7 +17,7 @@ }, "devDependencies": { "@biomejs/biome": "2.4.15", - "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/adapter-node": "^5.4.0", "@sveltejs/kit": "^2.57.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", "@tailwindcss/vite": "^4.2.2", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 2f03c27..271a9f8 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -21,9 +21,9 @@ importers: '@biomejs/biome': specifier: 2.4.15 version: 2.4.15 - '@sveltejs/adapter-static': - specifier: ^3.0.10 - version: 3.0.10(@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.10)(vite@8.0.14(@types/node@25.9.1)(jiti@2.7.0)))(svelte@5.55.10)(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(jiti@2.7.0))) + '@sveltejs/adapter-node': + specifier: ^5.4.0 + version: 5.5.4(@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.10)(vite@8.0.14(@types/node@25.9.1)(jiti@2.7.0)))(svelte@5.55.10)(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(jiti@2.7.0))) '@sveltejs/kit': specifier: ^2.57.0 version: 2.61.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.10)(vite@8.0.14(@types/node@25.9.1)(jiti@2.7.0)))(svelte@5.55.10)(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(jiti@2.7.0)) @@ -278,6 +278,180 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/plugin-commonjs@29.0.2': + resolution: {integrity: sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg==} + engines: {node: '>=16.0.0 || 14 >= 14.17'} + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-json@6.1.0': + resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-node-resolve@16.0.3': + resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.60.4': + resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.4': + resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.4': + resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.4': + resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.4': + resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.4': + resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.4': + resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.4': + resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.4': + resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.4': + resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.4': + resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.4': + resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.4': + resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.4': + resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} + cpu: [x64] + os: [win32] + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -286,10 +460,10 @@ packages: peerDependencies: acorn: ^8.9.0 - '@sveltejs/adapter-static@3.0.10': - resolution: {integrity: sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==} + '@sveltejs/adapter-node@5.5.4': + resolution: {integrity: sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ==} peerDependencies: - '@sveltejs/kit': ^2.0.0 + '@sveltejs/kit': ^2.4.0 '@sveltejs/kit@2.61.1': resolution: {integrity: sha512-Ny8s1SR1TyQS2hD2Rvw0XKzU2Nw1eUF52dTb6T2bdcgz7wSC+Nyb5IwjWYlR4b2dvbbR5NJDiQwHg3rnNseghg==} @@ -428,6 +602,9 @@ packages: '@types/d3-contour@3.0.6': resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} @@ -437,6 +614,9 @@ packages: '@types/node@25.9.1': resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -465,6 +645,9 @@ packages: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + cookie@0.6.0: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} @@ -602,6 +785,10 @@ packages: resolution: {integrity: sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==} engines: {node: '>=10.13.0'} + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + esm-env@1.2.2: resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} @@ -613,6 +800,9 @@ packages: '@typescript-eslint/types': optional: true + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -627,9 +817,16 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -641,6 +838,16 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + + is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} @@ -765,6 +972,9 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -780,6 +990,11 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} @@ -788,6 +1003,11 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rollup@4.60.4: + resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + runed@0.37.1: resolution: {integrity: sha512-MeFY73xBW8IueWBm012nNFIGy19WUGPLtknavyUPMpnyt350M47PhGSGrGoSLbidwn+Zlt/O0cp8/OZE3LASWA==} peerDependencies: @@ -821,6 +1041,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + svelte-check@4.4.8: resolution: {integrity: sha512-67adfgBox5eNSNIvIIwgFizKGdcRrGpiMoNO2obHcYuLz7iTa8Xgm/NGU3ntMFnNm8K1grFOIG6HhMLX/vcN8w==} engines: {node: '>= 18.0.0'} @@ -1094,15 +1318,130 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@rollup/plugin-commonjs@29.0.2(rollup@4.60.4)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.60.4) + commondir: 1.0.1 + estree-walker: 2.0.2 + fdir: 6.5.0(picomatch@4.0.4) + is-reference: 1.2.1 + magic-string: 0.30.21 + picomatch: 4.0.4 + optionalDependencies: + rollup: 4.60.4 + + '@rollup/plugin-json@6.1.0(rollup@4.60.4)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.60.4) + optionalDependencies: + rollup: 4.60.4 + + '@rollup/plugin-node-resolve@16.0.3(rollup@4.60.4)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.60.4) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.12 + optionalDependencies: + rollup: 4.60.4 + + '@rollup/pluginutils@5.3.0(rollup@4.60.4)': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.4 + optionalDependencies: + rollup: 4.60.4 + + '@rollup/rollup-android-arm-eabi@4.60.4': + optional: true + + '@rollup/rollup-android-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-x64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.4': + optional: true + '@standard-schema/spec@1.1.0': {} '@sveltejs/acorn-typescript@1.0.10(acorn@8.16.0)': dependencies: acorn: 8.16.0 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.10)(vite@8.0.14(@types/node@25.9.1)(jiti@2.7.0)))(svelte@5.55.10)(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(jiti@2.7.0)))': + '@sveltejs/adapter-node@5.5.4(@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.10)(vite@8.0.14(@types/node@25.9.1)(jiti@2.7.0)))(svelte@5.55.10)(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(jiti@2.7.0)))': dependencies: + '@rollup/plugin-commonjs': 29.0.2(rollup@4.60.4) + '@rollup/plugin-json': 6.1.0(rollup@4.60.4) + '@rollup/plugin-node-resolve': 16.0.3(rollup@4.60.4) '@sveltejs/kit': 2.61.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.10)(vite@8.0.14(@types/node@25.9.1)(jiti@2.7.0)))(svelte@5.55.10)(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(jiti@2.7.0)) + rollup: 4.60.4 '@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.10)(vite@8.0.14(@types/node@25.9.1)(jiti@2.7.0)))(svelte@5.55.10)(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(jiti@2.7.0))': dependencies: @@ -1222,6 +1561,8 @@ snapshots: '@types/d3-array': 3.2.2 '@types/geojson': 7946.0.16 + '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} '@types/geojson@7946.0.16': {} @@ -1230,6 +1571,8 @@ snapshots: dependencies: undici-types: 7.24.6 + '@types/resolve@1.20.2': {} + '@types/trusted-types@2.0.7': {} acorn@8.16.0: {} @@ -1246,6 +1589,8 @@ snapshots: commander@7.2.0: {} + commondir@1.0.1: {} + cookie@0.6.0: {} d3-array@2.12.1: @@ -1373,12 +1718,16 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.3 + es-errors@1.3.0: {} + esm-env@1.2.2: {} esrap@2.2.9: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + estree-walker@2.0.2: {} + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -1386,8 +1735,14 @@ snapshots: fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + graceful-fs@4.2.11: {} + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -1396,6 +1751,16 @@ snapshots: internmap@2.0.3: {} + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-module@1.0.0: {} + + is-reference@1.2.1: + dependencies: + '@types/estree': 1.0.9 + is-reference@3.0.3: dependencies: '@types/estree': 1.0.9 @@ -1511,6 +1876,8 @@ snapshots: obug@2.1.1: {} + path-parse@1.0.7: {} + picocolors@1.1.1: {} picomatch@4.0.4: {} @@ -1523,6 +1890,13 @@ snapshots: readdirp@4.1.2: {} + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + robust-predicates@3.0.3: {} rolldown@1.0.2: @@ -1546,6 +1920,37 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.2 '@rolldown/binding-win32-x64-msvc': 1.0.2 + rollup@4.60.4: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.4 + '@rollup/rollup-android-arm64': 4.60.4 + '@rollup/rollup-darwin-arm64': 4.60.4 + '@rollup/rollup-darwin-x64': 4.60.4 + '@rollup/rollup-freebsd-arm64': 4.60.4 + '@rollup/rollup-freebsd-x64': 4.60.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 + '@rollup/rollup-linux-arm-musleabihf': 4.60.4 + '@rollup/rollup-linux-arm64-gnu': 4.60.4 + '@rollup/rollup-linux-arm64-musl': 4.60.4 + '@rollup/rollup-linux-loong64-gnu': 4.60.4 + '@rollup/rollup-linux-loong64-musl': 4.60.4 + '@rollup/rollup-linux-ppc64-gnu': 4.60.4 + '@rollup/rollup-linux-ppc64-musl': 4.60.4 + '@rollup/rollup-linux-riscv64-gnu': 4.60.4 + '@rollup/rollup-linux-riscv64-musl': 4.60.4 + '@rollup/rollup-linux-s390x-gnu': 4.60.4 + '@rollup/rollup-linux-x64-gnu': 4.60.4 + '@rollup/rollup-linux-x64-musl': 4.60.4 + '@rollup/rollup-openbsd-x64': 4.60.4 + '@rollup/rollup-openharmony-arm64': 4.60.4 + '@rollup/rollup-win32-arm64-msvc': 4.60.4 + '@rollup/rollup-win32-ia32-msvc': 4.60.4 + '@rollup/rollup-win32-x64-gnu': 4.60.4 + '@rollup/rollup-win32-x64-msvc': 4.60.4 + fsevents: 2.3.3 + runed@0.37.1(@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.10)(vite@8.0.14(@types/node@25.9.1)(jiti@2.7.0)))(svelte@5.55.10)(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(jiti@2.7.0)))(svelte@5.55.10): dependencies: dequal: 2.0.3 @@ -1573,6 +1978,8 @@ snapshots: source-map-js@1.2.1: {} + supports-preserve-symlinks-flag@1.0.0: {} + svelte-check@4.4.8(picomatch@4.0.4)(svelte@5.55.10)(typescript@6.0.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs deleted file mode 100644 index 659927f..0000000 --- a/frontend/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub use include_dir::{Dir, File, include_dir}; - -pub static DIST: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/dist"); diff --git a/frontend/src/lib/components/AppShell.svelte b/frontend/src/lib/components/AppShell.svelte index a0898c8..9f04ff2 100644 --- a/frontend/src/lib/components/AppShell.svelte +++ b/frontend/src/lib/components/AppShell.svelte @@ -2,6 +2,7 @@ import { CloudAlert, Copyright } from "@lucide/svelte"; import { createQuery } from "@tanstack/svelte-query"; import type { Snippet } from "svelte"; +import { browser } from "$app/environment"; import type { StatusMetrics } from "$lib/api/status"; import { fetchStatus } from "$lib/api/status"; import { setStatusContext } from "$lib/context/status"; @@ -17,6 +18,10 @@ type CachedStatus = { }; function getCachedStatus() { + if (!browser) { + return null; + } + const cachedStatus = localStorage.getItem(statusCacheKey); if (!cachedStatus) { return null; @@ -40,7 +45,7 @@ const status = createQuery(() => ({ setStatusContext(status); $effect(() => { - if (!status.data) { + if (!browser || !status.data) { return; } diff --git a/frontend/src/routes/+layout.ts b/frontend/src/routes/+layout.ts index a3d1578..77ab0a0 100644 --- a/frontend/src/routes/+layout.ts +++ b/frontend/src/routes/+layout.ts @@ -1 +1 @@ -export const ssr = false; +export const ssr = true; diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js index 6b4ad82..b1f1e79 100644 --- a/frontend/svelte.config.js +++ b/frontend/svelte.config.js @@ -1,4 +1,4 @@ -import adapter from "@sveltejs/adapter-static"; +import adapter from "@sveltejs/adapter-node"; /** @type {import('@sveltejs/kit').Config} */ const config = { @@ -8,12 +8,7 @@ const config = { filename.split(/[/\\]/).includes("node_modules") ? undefined : true, }, kit: { - adapter: adapter({ - pages: "dist", - assets: "dist", - fallback: "index.html", - strict: true, - }), + adapter: adapter({ out: "build" }), }, }; diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..bb77953 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,42 @@ +upstream website_backend { + server website:8000; +} + +upstream frontend { + server frontend:3000; +} + +server { + listen 80; + server_name _; + + set_real_ip_from 172.18.0.0/16; + real_ip_header X-Forwarded-For; + real_ip_recursive on; + + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + location /api/v1/ { + proxy_pass http://website_backend; + } + + location /agency/ { + proxy_pass http://website_backend; + } + + location /whitelist/ { + proxy_pass http://website_backend; + } + + location /feedback/ { + proxy_pass http://website_backend/api/v1/feedback/; + } + + location / { + proxy_pass http://frontend; + } +} diff --git a/website/Cargo.toml b/website/Cargo.toml index 11593ef..be74022 100644 --- a/website/Cargo.toml +++ b/website/Cargo.toml @@ -11,7 +11,6 @@ rocket-client-addr = "0.5.4" serde = { workspace = true } reports = { path = "../reports" } querying = { path = "../querying" } -frontend = { path = "../frontend" } env_logger = "0.11.10" rocket-cache-response = "0.6.4" log = { workspace = true } diff --git a/website.Dockerfile b/website/Dockerfile similarity index 50% rename from website.Dockerfile rename to website/Dockerfile index fe942c8..4ddffbe 100644 --- a/website.Dockerfile +++ b/website/Dockerfile @@ -4,39 +4,21 @@ FROM docker.io/rust:1-slim-bookworm AS build WORKDIR /build -ENV PNPM_HOME=/pnpm -ENV PATH="${PNPM_HOME}:${PATH}" - RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ apt-get update && \ - apt-get install -y --no-install-recommends ca-certificates curl libssl-dev pkg-config && \ - curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && \ - apt-get install -y --no-install-recommends nodejs && \ - corepack enable && \ - corepack prepare pnpm@10.33.0 --activate && \ - pnpm config set store-dir /pnpm/store && \ + apt-get install -y --no-install-recommends ca-certificates libssl-dev pkg-config && \ rm -rf /var/lib/apt/lists/* -COPY frontend/package.json frontend/pnpm-lock.yaml frontend/pnpm-workspace.yaml ./frontend/ - -RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \ - pnpm --dir frontend fetch --frozen-lockfile - COPY . . RUN --mount=type=cache,id=website-target,target=/build/target \ --mount=type=cache,id=cargo-registry,target=/usr/local/cargo/registry \ --mount=type=cache,id=cargo-git,target=/usr/local/cargo/git \ - --mount=type=cache,id=pnpm-store,target=/pnpm/store \ set -eux; \ - pnpm --dir frontend install --frozen-lockfile --offline; \ - pnpm --dir frontend build; \ - RUSTFLAGS="-C strip=symbols" SKIP_FRONTEND_BUILD=true SQLX_OFFLINE=true cargo build --locked --release --package website; \ + RUSTFLAGS="-C strip=symbols" SQLX_OFFLINE=true cargo build --locked --release --package website; \ cp target/release/website ./main -################################################################################ - FROM docker.io/debian:bookworm-slim WORKDIR /app @@ -50,14 +32,12 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ useradd --system --gid app --home-dir /app --shell /usr/sbin/nologin app COPY --from=build /build/website/Rocket.toml ./ -## copy the main binary COPY --from=build /build/main ./ -## ensure the container listens globally on port 8080 ENV ROCKET_ADDRESS=:: -ENV ROCKET_PORT=8080 +ENV ROCKET_PORT=8000 -EXPOSE 8080 +EXPOSE 8000 USER app diff --git a/website/src/main.rs b/website/src/main.rs index ccba29a..59f5bdc 100644 --- a/website/src/main.rs +++ b/website/src/main.rs @@ -9,15 +9,13 @@ use env_logger::Env; use log::{LevelFilter, error, info}; use querying::Checker; use rocket::fairing::AdHoc; -use rocket::http::{ContentType, Status}; -use rocket::response::content::RawHtml; +use rocket::http::Status; use rocket::serde::json::Json; use rocket::tokio::sync::RwLock; use rocket::tokio::time; use rocket::{Build, Request, Rocket, fairing, tokio}; use serde::Serialize; use sqlx::postgres::PgPool; -use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; @@ -35,53 +33,6 @@ fn api_error(status: Status, _: &Request) -> Json { }) } -#[get("/")] -fn frontend_index() -> Option> { - frontend::DIST - .get_file("index.html") - .and_then(frontend::File::contents_utf8) - .map(RawHtml) -} - -#[get("/", rank = 20)] -fn frontend_asset(path: PathBuf) -> Option<(ContentType, &'static [u8])> { - if is_backend_path(&path) { - return None; - } - - if let Some(file) = embedded_file(&path) { - return Some((content_type(&path), file.contents())); - } - - if path.extension().is_none() { - let index = frontend::DIST.get_file("index.html")?; - return Some((ContentType::HTML, index.contents())); - } - - None -} - -fn embedded_file(path: &PathBuf) -> Option<&'static frontend::File<'static>> { - let path = path.to_string_lossy().replace('\\', "/"); - frontend::DIST.get_file(path) -} - -fn is_backend_path(path: &PathBuf) -> bool { - matches!( - path.components() - .next() - .and_then(|component| component.as_os_str().to_str()), - Some("api" | "agency" | "whitelist") - ) -} - -fn content_type(path: &PathBuf) -> ContentType { - path.extension() - .and_then(|extension| extension.to_str()) - .and_then(ContentType::from_extension) - .unwrap_or(ContentType::Binary) -} - async fn run_migrations(rocket: Rocket) -> fairing::Result { match rocket.state::() { Some(db) => match sqlx::migrate!("./migrations").run(db).await { @@ -158,7 +109,6 @@ async fn rocket() -> _ { .manage(pool) .manage(api_limiter) .attach(AdHoc::try_on_ignite("SQLx Migrations", run_migrations)) - .mount("/", routes![api::feedback]) // DEPRECATED: backwards compatibility .mount( "/api/v1", routes![ @@ -171,6 +121,5 @@ async fn rocket() -> _ { ) .mount("/agency", routes![agency::upload_report]) .mount("/whitelist", routes![whitelist::export_csv]) - .mount("/", routes![frontend_index, frontend_asset]) .register("/", catchers![api_error]) }