diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index f79823f0..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,253 +0,0 @@ -# Use the latest 2.1 version of CircleCI pipeline process engine. -# See: https://circleci.com/docs/2.0/configuration-reference -version: 2.1 - -# Define a job to be invoked later in a workflow. -# See: https://circleci.com/docs/2.0/configuration-reference/#jobs -jobs: - api-tests: - docker: - - image: cimg/python:3.13 - # `large` (4 vCPU, 8 GB) so pytest-xdist's `-nauto` has room to scale. - resource_class: large - steps: - - checkout - - run: sudo apt-get update - - run: sudo apt-get install -y ffmpeg catdoc - - restore_cache: - key: deps-3.13-{{ checksum "requirements.txt" }}-6 - - run: - name: Install Requirements - command: | - # A restored venv points at the exact interpreter that built it. CircleCI's - # cimg/python:3.13 gets patch bumps, and this cache key does not name the - # patch, so a venv cached before a bump has a dangling bin/python3 -- and - # `python3 -m venv` over it fails with `[Errno 2]` instead of repairing it, - # which broke every Python job. Rebuild when it cannot run: that costs a - # reinstall only on the first run after a bump. - if ! ./venv/bin/python3 --version >/dev/null 2>&1; then rm -rf venv; fi - python3 -m venv venv - . venv/bin/activate - pip install -r requirements.txt - - save_cache: - key: deps-3.13-{{ checksum "requirements.txt" }}-6 - paths: - - "venv" - # `-nauto` parallelizes across all available cores via pytest-xdist. - # `-svv` was dumping the full test stdout to CircleCI logs on every run, - # which inflated wall time and produced multi-MB log artifacts; dropped. - - run: - command: './venv/bin/pytest -nauto --junitxml=test-results.xml' - - store_test_results: - path: test-results.xml - # Do the API and the frontend's test fixtures still agree? - # - # `api-tests` runs these too, as part of the whole suite. This job exists for the - # signal: fixture drift is silent by nature -- the frontend suite passes against a body the - # API stopped sending -- so it is worth a check whose name says what broke, failing in under a - # minute rather than somewhere inside seventeen hundred results. - # - # It needs no ffmpeg or catdoc (verified), and shares the dependency cache key with the other - # Python jobs, so it costs a venv restore and one test file. - api-contract-tests: - docker: - - image: cimg/python:3.13 - resource_class: medium - steps: - - checkout - - restore_cache: - key: deps-3.13-{{ checksum "requirements.txt" }}-6 - - run: - name: Install Requirements - command: | - # A restored venv points at the exact interpreter that built it. CircleCI's - # cimg/python:3.13 gets patch bumps, and this cache key does not name the - # patch, so a venv cached before a bump has a dangling bin/python3 -- and - # `python3 -m venv` over it fails with `[Errno 2]` instead of repairing it, - # which broke every Python job. Rebuild when it cannot run: that costs a - # reinstall only on the first run after a bump. - if ! ./venv/bin/python3 --version >/dev/null 2>&1; then rm -rf venv; fi - python3 -m venv venv - . venv/bin/activate - pip install -r requirements.txt - - run: - name: Fixtures match the OpenAPI spec, and the spec matches the handlers - command: './venv/bin/pytest wrolpi/test/test_api_fixtures.py -v' - - api-alembic-migrations: - docker: - - image: cimg/python:3.13 - resource_class: medium - steps: - - checkout - - restore_cache: - key: deps-3.13-{{ checksum "requirements.txt" }}-6 - - run: - name: Install Requirements - command: | - # A restored venv points at the exact interpreter that built it. CircleCI's - # cimg/python:3.13 gets patch bumps, and this cache key does not name the - # patch, so a venv cached before a bump has a dangling bin/python3 -- and - # `python3 -m venv` over it fails with `[Errno 2]` instead of repairing it, - # which broke every Python job. Rebuild when it cannot run: that costs a - # reinstall only on the first run after a bump. - if ! ./venv/bin/python3 --version >/dev/null 2>&1; then rm -rf venv; fi - python3 -m venv venv - . venv/bin/activate - pip install -r requirements.txt - - run: - name: Create Media Directory - command: mkdir -p /tmp/wrolpi-media/config - - run: - name: Run Alembic Migrations - command: MEDIA_DIRECTORY=/tmp/wrolpi-media ./venv/bin/python ./main.py db upgrade - - run: - name: Check for Missing Migrations - command: MEDIA_DIRECTORY=/tmp/wrolpi-media ./venv/bin/python ./main.py db check - sanic-api-start: - docker: - - image: cimg/python:3.13 - steps: - - checkout - - restore_cache: - key: deps-3.13-{{ checksum "requirements.txt" }}-6 - - run: - name: Install dependencies - command: | - # A restored venv points at the exact interpreter that built it. CircleCI's - # cimg/python:3.13 gets patch bumps, and this cache key does not name the - # patch, so a venv cached before a bump has a dangling bin/python3 -- and - # `python3 -m venv` over it fails with `[Errno 2]` instead of repairing it, - # which broke every Python job. Rebuild when it cannot run: that costs a - # reinstall only on the first run after a bump. - if ! ./venv/bin/python3 --version >/dev/null 2>&1; then rm -rf venv; fi - python -m venv venv - . venv/bin/activate - pip install -r requirements.txt - sudo mkdir -p /media/wrolpi - - run: - name: Start Sanic API - command: | - . venv/bin/activate - sanic main:api_app -H 0.0.0.0 -p 8081 - background: true - - run: - name: Test Sanic API - command: | - sleep 20 - curl http://0.0.0.0:8081/api/echo - - run: - name: Stop Sanic API - command: pkill -f sanic - react-app-start: - docker: - - image: cimg/node:20.19 - resource_class: large - steps: - - checkout - - restore_cache: - key: app-{{ checksum "app/package-lock.json" }} - - run: cd app && npm install - - save_cache: - key: app-{{ checksum "app/package-lock.json" }} - paths: - - ./app/node_modules - - run: - name: Build React App - command: cd app && DISABLE_ESLINT_PLUGIN=true npm run build - - run: - name: Start React App - command: cd app && DISABLE_ESLINT_PLUGIN=true npm run start - background: true - - run: - name: Test React App - command: | - sleep 20 - curl http://0.0.0.0:3000/manifest.json - - run: - name: Stop React App - command: pkill -f npm - # Jest tests moved to GitHub Actions (.github/workflows/jest-tests.yml) - # Component specs live under `src/`, so they need `cypress run --component`; the e2e job's - # `cypress run` will not match them. Cypress starts its own dev server, so no `npm start`. - cypress-component-tests: - docker: - - image: cimg/node:20.19-browsers - resource_class: large - steps: - - checkout - - restore_cache: - key: app-browsers-{{ checksum "app/package-lock.json" }} - - run: - name: Install Dependencies - command: cd app && npm install - - run: - name: Install Cypress Binary - command: cd app && npx cypress install - - run: - name: Run Cypress Component Tests - command: cd app && CI=true npx cypress run --component - - store_artifacts: - path: app/cypress/screenshots - cypress-e2e-tests: - docker: - - image: cimg/node:20.19-browsers - resource_class: large - steps: - - checkout - - restore_cache: - key: app-browsers-{{ checksum "app/package-lock.json" }} - - run: - name: Install Dependencies - command: cd app && npm install - # The Cypress binary lives outside node_modules, so caching only - # node_modules lets the two drift apart: a restored cache satisfies npm, - # npm skips the postinstall download, and the binary for the new version - # is never fetched. Cache both, and install explicitly (a no-op when the - # binary is already present). - - run: - name: Install Cypress Binary - command: cd app && npx cypress install - - save_cache: - key: app-browsers-{{ checksum "app/package-lock.json" }} - paths: - - ./app/node_modules - - ~/.cache/Cypress - - run: - name: Start React App in Background - command: cd app && DISABLE_ESLINT_PLUGIN=true npm start - background: true - - run: - name: Wait for React App - command: | - for i in {1..60}; do - if curl -s http://localhost:3000 > /dev/null; then - echo "React app is ready!" - exit 0 - fi - echo "Waiting for React app... ($i/60)" - sleep 2 - done - echo "React app failed to start" - exit 1 - - run: - name: Run Cypress Tests - command: cd app && CI=true npx cypress run - - store_artifacts: - path: app/cypress/videos - - store_artifacts: - path: app/cypress/screenshots - -workflows: - wrolpi-api-tests: - jobs: - - sanic-api-start - - api-tests - - api-contract-tests - - api-alembic-migrations - wrolpi-app-test: - jobs: - - react-app-start - # jest-tests moved to GitHub Actions - - cypress-e2e-tests - - cypress-component-tests diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e1969755..29480843 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,11 @@ updates: interval: "monthly" target-branch: "master" open-pull-requests-limit: 5 + ignore: + # SQLAlchemy 2.1 is a minor release with its own deprecations; upgrade it + # deliberately rather than by an automatic bump. + - dependency-name: "SQLAlchemy" + versions: [">=2.1"] groups: python-dependencies: patterns: @@ -37,6 +42,42 @@ updates: interval: "monthly" target-branch: "master" open-pull-requests-limit: 5 + ignore: + # Major upgrades below need their own migration branch, not a bump PR. + # maplibre-gl 6: /map build depends on pinned babel/terser handling. + - dependency-name: "maplibre-gl" + versions: [">=6"] + # react-dropzone 15+ changed the accept API; 20 requires Node >=22. + - dependency-name: "react-dropzone" + versions: [">=15"] + # React 19: @react-three/fiber 8 pins react <19. + - dependency-name: "react" + update-types: ["version-update:semver-major"] + - dependency-name: "react-dom" + update-types: ["version-update:semver-major"] + - dependency-name: "@types/react" + update-types: ["version-update:semver-major"] + - dependency-name: "@types/react-dom" + update-types: ["version-update:semver-major"] + # TypeScript 6+/7 breaks the react-scripts toolchain. + - dependency-name: "typescript" + versions: [">=6"] + # Mantine 9: all @mantine packages move together with app code changes. + - dependency-name: "@mantine/*" + update-types: ["version-update:semver-major"] + # Cypress 16 requires Node >=22; CI and the app image run Node 20. + - dependency-name: "cypress" + versions: [">=16"] + # react-router 8 is a routing migration across ~40 files. + - dependency-name: "react-router" + update-types: ["version-update:semver-major"] + # @testing-library/react 14+ async act semantics break menu/popover tests. + - dependency-name: "@testing-library/react" + update-types: ["version-update:semver-major"] + # Cypress component testing resolves the top-level webpack-dev-server and only + # accepts major 5; 6 crashes cypress.config.js ("Unexpected major version"). + - dependency-name: "webpack-dev-server" + versions: [">=6"] groups: npm-dependencies: patterns: diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml new file mode 100644 index 00000000..05cec023 --- /dev/null +++ b/.github/workflows/backend.yml @@ -0,0 +1,145 @@ +name: Backend Tests + +# Same triggers as jest-tests.yml: `pull_request` is unfiltered so pull requests into feature +# branches are checked too, not only those into master. +on: + push: + branches: [master, feature/*] + pull_request: + +# A new push to a pull request makes the run for the previous push worthless; cancel it so it +# does not hold runners (the free plan allows 20 concurrent jobs). Runs on master always finish. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + # The suite is split by directory so it finishes in a few minutes, and so a check's name says + # which area broke. The groups are balanced by test time; `rest` runs everything the others do + # not, so a new directory is tested before anyone assigns it. A path added to any other group + # must also be added to `rest` as an --ignore, or its tests run twice. + api-tests: + name: api-tests (${{ matrix.name }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: core + paths: wrolpi/test + - name: media + paths: modules/videos modules/zim modules/docs modules/map modules/inventory modules/flasher + - name: files + paths: wrolpi/files wrolpi/collections + - name: rest + paths: >- + --ignore=wrolpi/test + --ignore=modules/videos --ignore=modules/zim --ignore=modules/docs --ignore=modules/map + --ignore=modules/inventory --ignore=modules/flasher + --ignore=wrolpi/files --ignore=wrolpi/collections + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install system packages + run: sudo apt-get update && sudo apt-get install -y ffmpeg catdoc + + - name: Setup Python + uses: actions/setup-python@v5 + with: + # Trixie ships 3.13. + python-version: '3.13' + cache: 'pip' + cache-dependency-path: requirements.txt + + - name: Install dependencies + run: pip install -r requirements.txt + + # `logical`, not `auto`: with psutil installed, `auto` counts physical cores, which is two on + # the four-vCPU runner. No `-v`: the full test output inflates wall time and logs. + - name: Run pytest + env: + PATHS: ${{ matrix.paths }} + run: pytest -n logical $PATHS --junitxml=test-results.xml + + - name: Upload test results + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: api-test-results-${{ matrix.name }} + path: test-results.xml + retention-days: 30 + + # Do the API and the frontend's test fixtures still agree? + # + # `api-tests` runs these too, as part of the whole suite. This job exists for the signal: + # fixture drift is silent by nature -- the frontend suite passes against a body the API stopped + # sending -- so it is worth a check whose name says what broke, failing in under a minute rather + # than somewhere inside seventeen hundred results. It needs no ffmpeg or catdoc. + api-contract-tests: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + cache: 'pip' + cache-dependency-path: requirements.txt + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Fixtures match the OpenAPI spec, and the spec matches the handlers + run: pytest wrolpi/test/test_api_fixtures.py -v + + # The migrations apply to an empty database, the models match them, and the API starts. + api-migrations-and-start: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + cache: 'pip' + cache-dependency-path: requirements.txt + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Run Alembic migrations + env: + MEDIA_DIRECTORY: ${{ runner.temp }}/wrolpi-media + run: | + mkdir -p "$MEDIA_DIRECTORY/config" + python ./main.py db upgrade + + - name: Check for missing migrations + env: + MEDIA_DIRECTORY: ${{ runner.temp }}/wrolpi-media + run: python ./main.py db check + + - name: Start Sanic API + run: | + sudo mkdir -p /media/wrolpi + nohup sanic main:api_app -H 0.0.0.0 -p 8081 > sanic.log 2>&1 & + for i in $(seq 1 60); do + if curl -sf http://localhost:8081/api/echo; then + echo + echo "Sanic API is up" + exit 0 + fi + sleep 2 + done + echo "Sanic API failed to start" + cat sanic.log + exit 1 + + - name: Stop Sanic API + if: ${{ always() }} + run: pkill -f sanic || true diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml new file mode 100644 index 00000000..efb01e97 --- /dev/null +++ b/.github/workflows/cypress.yml @@ -0,0 +1,121 @@ +name: Cypress Tests + +on: + push: + branches: [master, feature/*] + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + # Component specs live under `src/`, so they need `cypress run --component`; the e2e job's + # `cypress run` will not match them. Cypress starts its own dev server, so no `npm start`. + cypress-component-tests: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: app + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.19' + cache: 'npm' + cache-dependency-path: app/package-lock.json + + # The Cypress binary lives outside node_modules, and setup-node only caches ~/.npm. Key it + # on the lockfile so a Cypress bump fetches the matching binary. + - name: Cache Cypress binary + uses: actions/cache@v4 + with: + path: ~/.cache/Cypress + key: cypress-${{ runner.os }}-${{ hashFiles('app/package-lock.json') }} + + - name: Install dependencies + run: npm install + + # A no-op when the cached binary matches. + - name: Install Cypress binary + run: npx cypress install + + - name: Run Cypress component tests + run: npx cypress run --component + + - name: Upload screenshots + if: ${{ failure() }} + uses: actions/upload-artifact@v4 + with: + name: cypress-component-screenshots + path: app/cypress/screenshots + retention-days: 14 + + # Waiting for `npm start` to serve the app also checks that the dev server starts. + cypress-e2e-tests: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: app + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.19' + cache: 'npm' + cache-dependency-path: app/package-lock.json + + - name: Cache Cypress binary + uses: actions/cache@v4 + with: + path: ~/.cache/Cypress + key: cypress-${{ runner.os }}-${{ hashFiles('app/package-lock.json') }} + + - name: Install dependencies + run: npm install + + - name: Install Cypress binary + run: npx cypress install + + # GitHub Actions sets `CI=true`, which makes cypress.config.js use http://localhost:3000 as + # the base URL. + - name: Start React app + env: + DISABLE_ESLINT_PLUGIN: 'true' + BROWSER: none + run: | + nohup npm start > react.log 2>&1 & + for i in $(seq 1 90); do + if curl -sf http://localhost:3000/manifest.json > /dev/null; then + echo "React app is ready" + exit 0 + fi + sleep 2 + done + echo "React app failed to start" + cat react.log + exit 1 + + - name: Run Cypress e2e tests + run: npx cypress run + + - name: Upload videos and screenshots + if: ${{ failure() }} + uses: actions/upload-artifact@v4 + with: + name: cypress-e2e-media + path: | + app/cypress/videos + app/cypress/screenshots + retention-days: 14 + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 655c907f..a3ab9bc5 100644 --- a/.gitignore +++ b/.gitignore @@ -150,3 +150,6 @@ app/cypress/videos release/latest.json perf_results/ scripts/api_perf_hosts.json + +# Tile cache for scripts/build_solar_climatology.py +/scripts/.solar_climatology_cache/ diff --git a/INSTALL.md b/INSTALL.md index 1fc77ee8..cf93f460 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -3,8 +3,10 @@ There are several ways to run WROLPi. Pick the one that matches your hardware: * [WROLPi Portable (Live USB)](#wrolpi-portable-live-usb) — boot a full WROLPi from a USB stick on any x86 PC or laptop, no installation required. -* [Debian 12 Install](#debian-12-install) — install WROLPi onto an x86 PC or laptop. +* [Debian 13 Install](#debian-13-install) — install WROLPi onto an x86 PC or laptop. * [Raspberry Pi Install](#raspberry-pi-install) — install WROLPi onto a Raspberry Pi with an external drive. + +After installing, follow [First Boot](#first-boot) to set up your drive. * [Docker](docker/README.md) — run the WROLPi containers on an existing Linux host (best for trying it out or development). See [UPGRADE.md](UPGRADE.md) for upgrading an existing WROLPi. @@ -36,6 +38,7 @@ Steps: minute or two; progress is shown on screen. 5. Once the desktop loads, browse to https://wrolpi.local or the IP address of your WROLPi. Wi-Fi hotspot and all other features work as usual. +6. To use a terminal remotely: `ssh user@wrolpi.local`, password `live`. Notes: @@ -52,30 +55,50 @@ Notes: * **Upgrading.** See [Upgrading a WROLPi Portable USB](UPGRADE.md#upgrading-a-wrolpi-portable-usb) to move to a newer ISO without losing your library. -## Debian 12 Install +## Debian 13 Install -Steps necessary to initialize your WROLPi after installing the Debian image from wrolpi.org +Install WROLPi onto the internal disk of an x86 PC or laptop. This uses the same +`WROLPi-v*-amd64.iso` as WROLPi Portable. -1. Download and copy a pre-built Debian image from https://wrolpi.org onto a USB thumb-drive (USB 2 recommended). -2. Insert the thumb-drive into the laptop, boot to the thumb-drive. - 1. Select "Start Installer". - 2. Install Debian 12 as you would like. - 1. It is recommended to use the hostname **wrolpi**. - 2. (WROLPi will be installed during the installation without your intervention.) -3. Unplug the thumb-drive after the installation has completed. -4. Login as the user _you_ created during installation. -5. Initialize the WROLPi databases using the repair script: `/opt/wrolpi/repair.sh` -6. Reboot: `reboot` -7. Browse to https://wrolpi.local or the IP address of your WROLPi! +1. Download the latest `WROLPi-v*-amd64.iso` from [wrolpi.org](https://wrolpi.org) and flash it to a USB drive + (see [WROLPi Portable](#wrolpi-portable-live-usb) step 2). +2. Boot the PC from the USB drive. At the boot menu, choose **Install** or **Graphical Install**. +3. Install Debian 13 as you would like. + 1. It is recommended to use the hostname **wrolpi**. + 2. WROLPi is installed along with Debian; there is nothing extra to select. +4. Unplug the USB drive after the installation has completed, and reboot. +5. Continue with [First Boot](#first-boot). ## Raspberry Pi Install -Steps necessary to initialize your WROLPi after installing the Raspberry Pi image from wrolpi.org +Install WROLPi onto a Raspberry Pi. An external drive is recommended for your library. -1. Download and copy a pre-built image from https://wrolpi.org onto an SD card. -2. Boot the Raspberry Pi, login with username pi and password wrolpi. -3. Modify fstab to mount your external drive to /media/wrolpi (modify this command to match your system). - * `echo '/dev/sda1 /media/wrolpi auto defaults,nofail 0 0' | sudo tee -a /etc/fstab` -4. Initialize the WROLPi databases using the repair script: `sudo /opt/wrolpi/repair.sh` -5. Reboot `sudo reboot` -6. Join the Hotspot or browse to https://wrolpi.local or the IP address of your WROLPi! +1. Download the latest `WROLPi-v*-aarch64-desktop.img.xz` from [wrolpi.org](https://wrolpi.org). +2. Flash it to an SD card using Raspberry Pi Imager. +3. Plug in your external drive, insert the SD card, and boot the Raspberry Pi. +4. On the first boot, Raspberry Pi OS asks you to create your own user. +5. Continue with [First Boot](#first-boot). + +## First Boot + +These steps are the same for every install. + +1. Join the WROLPi hotspot (network **WROLPi**, password **wrolpi hotspot**; it starts on every boot), or connect + your WROLPi to your network, then browse to https://wrolpi.local (or the IP address of your WROLPi). +2. **Choose your drive.** + * **Raspberry Pi and Debian:** until a drive is set up, you will see the **Controller** with a *Welcome to WROLPi* + wizard (also at http://wrolpi.local). Select the drive which will hold your library; WROLPi mounts it + at `/media/wrolpi`, adds it to `/etc/fstab`, and runs the repair script for you. If the drive already holds a + WROLPi library (from another WROLPi, or a backup), its configuration is used. No external drive? Choose **Run + repair without external drive** to keep your library on the internal disk or SD card. + * **WROLPi Portable:** nothing to do; your library lives in the persistence partition on the USB drive. +3. **Trust the certificate.** Your browser will warn about the HTTPS certificate because it was made by your WROLPi, + not by a public authority. Download https://wrolpi.local/ca.crt and trust it on each device you use; see + *Help > Certificates* on your WROLPi for per-device instructions. +4. **Wait for the first refresh.** WROLPi refreshes all your files when it first starts so it can find anything already + on the drive. Your library looks empty, and Download and Upload are disabled, until the refresh completes. +5. Start adding content: download a video channel, archive a web page, or add a Zim (Wikipedia) file. The *Help* page + on your WROLPi explains each module. + +You do not need to edit `/etc/fstab` or run `repair.sh` by hand. `sudo /opt/wrolpi/repair.sh` is still available if +your WROLPi ever needs repairing. diff --git a/README.md b/README.md index dfe580fd..291d2507 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # WROLPi -[![Build Status](https://img.shields.io/circleci/build/github/lrnselfreliance/wrolpi/release?label=release%20build)](https://app.circleci.com/pipelines/github/lrnselfreliance/wrolpi?branch=release&filter=all) +[![Backend Tests](https://github.com/lrnselfreliance/wrolpi/actions/workflows/backend.yml/badge.svg?branch=master)](https://github.com/lrnselfreliance/wrolpi/actions/workflows/backend.yml?query=branch%3Amaster) [![License: GPL v3](https://img.shields.io/github/license/lrnselfreliance/wrolpi?style=flat-square)](https://github.com/lrnselfreliance/wrolpi/blob/master/LICENSE) [![Discord](https://img.shields.io/discord/900430987987681330?label=Discord&logo=discord)](https://discord.gg/HrwFk7nqA2) [![Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/lrnselfreliance/wrolpi) @@ -93,7 +93,7 @@ More Docker documentation is available in [docker/README.md](docker/README.md) Full installation instructions are in [INSTALL.md](INSTALL.md). Pick the option that matches your hardware: * **[WROLPi Portable (Live USB)](INSTALL.md#wrolpi-portable-live-usb)** — boot a complete WROLPi from a USB stick on any x86 PC or laptop. Nothing is installed to the computer; the first boot creates a persistence partition on the USB so your library, database, and configuration survive reboots. -* **[Debian 12](INSTALL.md#debian-12-install)** — install WROLPi onto an x86 PC or laptop. +* **[Debian 13](INSTALL.md#debian-13-install)** — install WROLPi onto an x86 PC or laptop. * **[Raspberry Pi](INSTALL.md#raspberry-pi-install)** — install WROLPi onto a Raspberry Pi with an external drive. * **[Docker](docker/README.md)** — run the WROLPi containers on an existing Linux host. diff --git a/UPGRADE.md b/UPGRADE.md index 0b3449af..7029bb37 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -3,7 +3,7 @@ # Table of Contents * [Upgrading Docker containers](#upgrading-docker-containers) -* [Upgrading Raspberry Pi or Debian 12 Installation](#upgrading-raspberry-pi-or-debian-12-installation) +* [Upgrading Raspberry Pi or Debian Installation](#upgrading-raspberry-pi-or-debian-installation) * [Upgrading a WROLPi Portable USB](#upgrading-a-wrolpi-portable-usb) ## Upgrading Docker containers @@ -24,7 +24,9 @@ 7. Start all docker containers * `docker-compose up -d` -## Upgrading Raspberry Pi or Debian 12 Installation +## Upgrading Raspberry Pi or Debian Installation + +A Debian 12 (bookworm) WROLPi stays on the final version for Debian 12; reinstall on Debian 13 to get newer versions. 1. Run the upgrade script * `sudo /bin/bash /opt/wrolpi/upgrade.sh` diff --git a/alembic/env.py b/alembic/env.py index 9aeeb116..1fc7bd9c 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -6,9 +6,10 @@ from alembic import context # access to the values within the .ini file in use. config = context.config -# Interpret the config file for Python logging. -# This line sets up loggers basically. -fileConfig(config.config_file_name) +# Configure logging only for the alembic command line. WROLPi also migrates in-process at startup +# (db_bootstrap), where fileConfig would replace the API's handlers and disable its existing loggers. +if config.cmd_opts is not None: + fileConfig(config.config_file_name) # add your model's MetaData object here # for 'autogenerate' support @@ -23,6 +24,7 @@ from wrolpi.collections.models import Collection, CollectionItem # noqa: F401 from modules.videos.models import Video, Channel # noqa: F401 from modules.archive.models import Archive # noqa: F401 from modules.zim.models import Zim, TagZimEntry, ZimSubscription # noqa: F401 +from modules.repos.models import Repository # noqa: F401 target_metadata = Base.metadata @@ -96,7 +98,10 @@ def run_migrations_online(): connectable = create_wrolpi_engine(get_url()) - with connectable.connect() as connection: + # Autocommit: each migration statement is its own transaction. A migration may call WROLPi + # functions which write on their own connection; a migration connection holding an open read + # transaction could not then upgrade to a writer ("database is locked"). + with connectable.connect().execution_options(isolation_level='AUTOCOMMIT') as connection: context.configure( connection=connection, target_metadata=target_metadata, diff --git a/alembic/versions/2026_09_27_1200_domain_collection_summary.py b/alembic/versions/2026_09_27_1200_domain_collection_summary.py new file mode 100644 index 00000000..405dbfe1 --- /dev/null +++ b/alembic/versions/2026_09_27_1200_domain_collection_summary.py @@ -0,0 +1,45 @@ +"""Maintain collection.item_count/total_size for Domain Collections with triggers. + +Listing Domain Collections summed file_group.size through an archive join on every request, +one random page read per archive. The new triggers keep the summary columns current +(as the channel triggers already do for Channels); this migration installs them and +backfills existing Domain Collections. + +Revision ID: 2026_09_27_1200 +Revises: 2026_09_20_1200 +""" +from alembic import op + +# revision identifiers, used by Alembic. +revision = '2026_09_27_1200' +down_revision = '2026_09_20_1200' +branch_labels = None +depends_on = None + +TRIGGERS = ( + 'archive_insert_collection_summary', + 'archive_delete_collection_summary', + 'archive_update_collection_summary', + 'file_group_size_collection_summary', +) + + +def upgrade(): + from wrolpi.schema_ddl import install_raw_ddl + + # Idempotent: only the new triggers are created. + install_raw_ddl(op.get_bind()) + + op.execute(''' + UPDATE collection SET + item_count = (SELECT COUNT(*) FROM archive WHERE collection_id = collection.id), + total_size = (SELECT COALESCE(SUM(fg.size), 0) FROM archive a + LEFT JOIN file_group fg ON a.file_group_id = fg.id + WHERE a.collection_id = collection.id) + WHERE kind = 'domain' + ''') + + +def downgrade(): + for name in TRIGGERS: + op.execute(f'DROP TRIGGER IF EXISTS {name}') diff --git a/alembic/versions/2026_09_28_1200_collection_listing_indexes.py b/alembic/versions/2026_09_28_1200_collection_listing_indexes.py new file mode 100644 index 00000000..d8ca602b --- /dev/null +++ b/alembic/versions/2026_09_28_1200_collection_listing_indexes.py @@ -0,0 +1,29 @@ +"""Covering indexes for the Collection listing. + +Listing Collections read every channel row (for video_count/total_size) and every download +row of those Collections (for the minimum frequency). Both tables store yt-dlp info_json, +so their rows are tens of KB and each sits on its own scattered page; from a cold disk cache +that was 10+ seconds. These indexes answer both queries without touching a row. + +Revision ID: 2026_09_28_1200 +Revises: 2026_09_27_1200 +""" +from alembic import op + +# revision identifiers, used by Alembic. +revision = '2026_09_28_1200' +down_revision = '2026_09_27_1200' +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute('CREATE INDEX IF NOT EXISTS channel_collection_summary_idx' + ' ON channel (collection_id, video_count, total_size)') + op.execute('CREATE INDEX IF NOT EXISTS download_collection_frequency_idx' + ' ON download (collection_id, frequency)') + + +def downgrade(): + op.execute('DROP INDEX IF EXISTS channel_collection_summary_idx') + op.execute('DROP INDEX IF EXISTS download_collection_frequency_idx') diff --git a/alembic/versions/2026_10_05_1200_markdown_mimetype.py b/alembic/versions/2026_10_05_1200_markdown_mimetype.py new file mode 100644 index 00000000..02300ea4 --- /dev/null +++ b/alembic/versions/2026_10_05_1200_markdown_mimetype.py @@ -0,0 +1,31 @@ +"""Retag existing markdown file groups as text/markdown. + +A refresh skips a group whose files have not changed, so markdown indexed before text/markdown +existed would keep the type libmagic gave it (text/plain, or a programming language when the file +holds code) and the indexer that type chose. Marking the rows unindexed lets apply_indexers +re-index them with TextIndexer. SQLite's LIKE is case-insensitive for ASCII, so `.MD` matches. + +Revision ID: 2026_10_05_1200 +Revises: 2026_09_28_1200 +""" +from alembic import op + +# revision identifiers, used by Alembic. +revision = '2026_10_05_1200' +down_revision = '2026_09_28_1200' +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute(""" + UPDATE file_group SET mimetype = 'text/markdown', "indexed" = 0 + WHERE (primary_path LIKE '%.md' OR primary_path LIKE '%.markdown') + AND mimetype LIKE 'text/%' + AND mimetype != 'text/markdown' + """) + + +def downgrade(): + # The original types are not recorded; text/plain is what libmagic reports for most markdown. + op.execute("""UPDATE file_group SET mimetype = 'text/plain', "indexed" = 0 WHERE mimetype = 'text/markdown'""") diff --git a/alembic/versions/2026_10_06_1200_repository.py b/alembic/versions/2026_10_06_1200_repository.py new file mode 100644 index 00000000..e4537267 --- /dev/null +++ b/alembic/versions/2026_10_06_1200_repository.py @@ -0,0 +1,43 @@ +"""Add the repository table for git Repos (Collections with kind='repo'). + +Revision ID: 2026_10_06_1200 +Revises: 2026_10_05_1200 +""" +import sqlalchemy as sa +from alembic import op + +import wrolpi.dates + +# revision identifiers, used by Alembic. +revision = '2026_10_06_1200' +down_revision = '2026_10_05_1200' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + 'repository', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('url', sa.String(), nullable=False), + sa.Column('host', sa.String(), nullable=True), + sa.Column('owner', sa.String(), nullable=True), + sa.Column('mode', sa.String(), nullable=False), + sa.Column('branch', sa.String(), nullable=True), + sa.Column('default_branch', sa.String(), nullable=True), + sa.Column('head_sha', sa.String(), nullable=True), + sa.Column('head_date', wrolpi.dates.TZDateTime(), nullable=True), + sa.Column('head_message', sa.Text(), nullable=True), + sa.Column('last_fetch', wrolpi.dates.TZDateTime(), nullable=True), + sa.Column('size', sa.BigInteger(), nullable=True), + sa.Column('readme_path', sa.String(), nullable=True), + sa.Column('collection_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['collection_id'], ['collection.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('collection_id'), + sa.UniqueConstraint('url'), + ) + + +def downgrade(): + op.drop_table('repository') diff --git a/alembic/versions/2026_10_06_1300_repository_search.py b/alembic/versions/2026_10_06_1300_repository_search.py new file mode 100644 index 00000000..b4fd7abc --- /dev/null +++ b/alembic/versions/2026_10_06_1300_repository_search.py @@ -0,0 +1,39 @@ +"""Search git Repos by name and README: repository.search_name/readme_text and repository_fts. + +Revision ID: 2026_10_06_1300 +Revises: 2026_10_06_1200 +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '2026_10_06_1300' +down_revision = '2026_10_06_1200' +branch_labels = None +depends_on = None + + +def upgrade(): + from wrolpi.schema_ddl import install_raw_ddl + + op.add_column('repository', sa.Column('search_name', sa.String(), nullable=True)) + op.add_column('repository', sa.Column('readme_text', sa.Text(), nullable=True)) + op.execute(''' + UPDATE repository SET search_name = TRIM( + COALESCE((SELECT name FROM collection WHERE collection.id = repository.collection_id), '') + || ' ' || COALESCE(owner, '')) + ''') + + # Creates repository_fts and its triggers now that the columns exist. The README text is filled by the next + # update of each Repo. + install_raw_ddl(op.get_bind()) + op.execute("INSERT INTO repository_fts(repository_fts) VALUES('rebuild')") + + +def downgrade(): + for name in ('repository_fts_ai', 'repository_fts_ad', 'repository_fts_au'): + op.execute(f'DROP TRIGGER IF EXISTS {name}') + op.execute('DROP TABLE IF EXISTS repository_fts') + with op.batch_alter_table('repository') as batch_op: + batch_op.drop_column('readme_text') + batch_op.drop_column('search_name') diff --git a/alembic/versions/2026_10_06_1400_repository_submodules.py b/alembic/versions/2026_10_06_1400_repository_submodules.py new file mode 100644 index 00000000..123ba97d --- /dev/null +++ b/alembic/versions/2026_10_06_1400_repository_submodules.py @@ -0,0 +1,22 @@ +"""A git Repo can also download its submodules: repository.submodules. + +Revision ID: 2026_10_06_1400 +Revises: 2026_10_06_1300 +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '2026_10_06_1400' +down_revision = '2026_10_06_1300' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('repository', sa.Column('submodules', sa.Boolean(), server_default=sa.false(), nullable=False)) + + +def downgrade(): + with op.batch_alter_table('repository') as batch_op: + batch_op.drop_column('submodules') diff --git a/alembic/versions/2026_10_06_1500_repository_clone_token.py b/alembic/versions/2026_10_06_1500_repository_clone_token.py new file mode 100644 index 00000000..61d936af --- /dev/null +++ b/alembic/versions/2026_10_06_1500_repository_clone_token.py @@ -0,0 +1,22 @@ +"""A git Repo's clone holds a random token: repository.clone_token. + +Revision ID: 2026_10_06_1500 +Revises: 2026_10_06_1400 +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '2026_10_06_1500' +down_revision = '2026_10_06_1400' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('repository', sa.Column('clone_token', sa.String(), nullable=True)) + + +def downgrade(): + with op.batch_alter_table('repository') as batch_op: + batch_op.drop_column('clone_token') diff --git a/alembic/versions/2026_10_08_1200_file_group_progress.py b/alembic/versions/2026_10_08_1200_file_group_progress.py new file mode 100644 index 00000000..5742beee --- /dev/null +++ b/alembic/versions/2026_10_08_1200_file_group_progress.py @@ -0,0 +1,24 @@ +"""A FileGroup records how far the user is through it: file_group.progress and file_group.position. + +Revision ID: 2026_10_08_1200 +Revises: 2026_10_06_1500 +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '2026_10_08_1200' +down_revision = '2026_10_06_1500' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('file_group', sa.Column('progress', sa.Float(), nullable=True)) + op.add_column('file_group', sa.Column('position', sa.JSON(), nullable=True)) + + +def downgrade(): + # Not batch_alter_table: rebuilding file_group drops its FTS triggers (see 2026_09_20_1200). + op.execute('ALTER TABLE file_group DROP COLUMN position') + op.execute('ALTER TABLE file_group DROP COLUMN progress') diff --git a/alembic/versions/2026_10_24_1200_config_version.py b/alembic/versions/2026_10_24_1200_config_version.py new file mode 100644 index 00000000..bf202b50 --- /dev/null +++ b/alembic/versions/2026_10_24_1200_config_version.py @@ -0,0 +1,27 @@ +"""Each database config's version, so an import can tell whether the database or the file is newer +(see wrolpi.config_version). + +Revision ID: 2026_10_24_1200 +Revises: 2026_10_08_1200 +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '2026_10_24_1200' +down_revision = '2026_10_08_1200' +branch_labels = None +depends_on = None + + +def upgrade(): + # No rows: a config's next import (without deletes) records its version. + op.create_table( + 'config_version', + sa.Column('name', sa.String(), primary_key=True), + sa.Column('version', sa.Integer(), nullable=False), + ) + + +def downgrade(): + op.drop_table('config_version') diff --git a/alembic/versions/2026_10_25_1200_strip_tag_names.py b/alembic/versions/2026_10_25_1200_strip_tag_names.py new file mode 100644 index 00000000..a076bbf8 --- /dev/null +++ b/alembic/versions/2026_10_25_1200_strip_tag_names.py @@ -0,0 +1,175 @@ +"""Tag names have no leading or trailing whitespace. + +A Tag whose name had whitespace is renamed. Tags whose names are the same once stripped are merged into one: it +keeps every tagged file, Zim entry and Collection, and the first color (by Tag id) of the merged Tags. Downloads +and the config files that name Tags are rewritten the same way, so the next import finds them in step with the +database (an import of the old names would create the padded Tags again, or delete the stripped ones). + +Revision ID: 2026_10_25_1200 +Revises: 2026_10_24_1200 +""" +import json +import logging + +import yaml +from alembic import op + +from wrolpi.common import get_media_directory, write_config_data + +# revision identifiers, used by Alembic. +revision = '2026_10_25_1200' +down_revision = '2026_10_24_1200' +branch_labels = None +depends_on = None + +logger = logging.getLogger(__name__) + +# The configs which name Tags. +CONFIG_FILES = ('tags.yaml', 'download_manager.yaml', 'channels.yaml', 'domains.yaml', 'playlists.yaml', + 'repos.yaml', 'videos_downloader.yaml', 'archives_downloader.yaml') + + +def _strip(name): + """A Tag name without its whitespace; a name that would be empty is kept (it cannot be renamed).""" + if isinstance(name, str) and name.strip(): + return name.strip() + return name + + +def _strip_names(names: list) -> list: + stripped = list() + for name in map(_strip, names): + if name not in stripped: + stripped.append(name) + return stripped + + +def _strip_nested(data): + """Strip the Tag names in a config's (or a Download's settings') `tag_name`, `tag_names` and + `channel_tag_name` values, at any depth.""" + if isinstance(data, list): + return [_strip_nested(i) for i in data] + if not isinstance(data, dict): + return data + result = dict() + for key, value in data.items(): + if key == 'tag_name': + value = _strip(value) + elif key in ('tag_names', 'channel_tag_name') and isinstance(value, list): + value = _strip_names(value) + else: + value = _strip_nested(value) + result[key] = value + return result + + +def _strip_rows(rows: list, key_length: int) -> list: + """tags.yaml's tag_files or tag_zims, each row keyed by its first `key_length` values (the Tag name first).""" + result, seen = list(), set() + for row in rows: + if isinstance(row, list) and row: + row = [_strip(row[0]), *row[1:]] + key = json.dumps(row[:key_length], default=str) + if key in seen: + continue + seen.add(key) + result.append(row) + return result + + +def _strip_tags_config(config: dict, colors: dict) -> dict: + """`colors`: each renamed Tag's color in the database, which the file must match (an import copies the file's + color into the database).""" + config = dict(config) + if isinstance(config.get('tags'), dict): + tags = dict() + for name, attrs in config['tags'].items(): + stripped = _strip(name) + if stripped not in tags: + tags[stripped] = attrs + elif isinstance(attrs, dict) and attrs.get('color') and not (tags[stripped] or dict()).get('color'): + # The first color of the merged Tags. + tags[stripped] = {**(tags[stripped] or dict()), 'color': attrs['color']} + for name in tags.keys() & colors.keys(): + tags[name] = {**(tags[name] or dict()), 'color': colors[name]} + config['tags'] = tags + if isinstance(config.get('tag_files'), list): + config['tag_files'] = _strip_rows(config['tag_files'], 2) + if isinstance(config.get('tag_zims'), list): + config['tag_zims'] = _strip_rows(config['tag_zims'], 3) + return config + + +def strip_tag_names_in_configs(colors: dict): + config_directory = get_media_directory() / 'config' + for file_name in CONFIG_FILES: + file = config_directory / file_name + if not file.is_file(): + continue + try: + with file.open('rt') as fh: + config = yaml.load(fh, Loader=yaml.CSafeLoader) + except Exception as e: + logger.error(f'Cannot strip Tag names of invalid config: {file}', exc_info=e) + continue + if not isinstance(config, dict): + continue + stripped = _strip_tags_config(config, colors) if file_name == 'tags.yaml' else config + stripped = _strip_nested(stripped) + if stripped != config: + logger.warning(f'Stripping whitespace from Tag names in {file}') + write_config_data(stripped, file, width=500 if file_name == 'tags.yaml' else 90) + + +def strip_tag_names_in_db(connection) -> dict: + """Returns the color of each renamed Tag, by its new name.""" + groups, colors = dict(), dict() + for id_, name, color in connection.exec_driver_sql('SELECT id, name, color FROM tag ORDER BY id').fetchall(): + groups.setdefault(_strip(name), list()).append((id_, name, color)) + + for stripped, tags in groups.items(): + if all(name == stripped for _, name, _ in tags): + continue + # Keep the Tag already named this, if any, otherwise the oldest. + survivor_id = next((id_ for id_, name, _ in tags if name == stripped), tags[0][0]) + color = colors[stripped] = next((color for _, _, color in tags if color), None) + for id_, name, _ in tags: + if id_ == survivor_id: + continue + logger.warning(f'Merging Tag {name!r} into {stripped!r}') + connection.exec_driver_sql( + 'INSERT OR IGNORE INTO tag_file (tag_id, file_group_id, created_at)' + ' SELECT ?, file_group_id, created_at FROM tag_file WHERE tag_id = ?', (survivor_id, id_)) + connection.exec_driver_sql( + 'INSERT OR IGNORE INTO tag_zim (tag_id, zim_id, zim_entry, created_at)' + ' SELECT ?, zim_id, zim_entry, created_at FROM tag_zim WHERE tag_id = ?', (survivor_id, id_)) + connection.exec_driver_sql('DELETE FROM tag_file WHERE tag_id = ?', (id_,)) + connection.exec_driver_sql('DELETE FROM tag_zim WHERE tag_id = ?', (id_,)) + connection.exec_driver_sql('UPDATE collection SET tag_id = ? WHERE tag_id = ?', (survivor_id, id_)) + connection.exec_driver_sql('DELETE FROM tag WHERE id = ?', (id_,)) + logger.warning(f'Renaming Tag to {stripped!r}') + connection.exec_driver_sql('UPDATE tag SET name = ?, color = ? WHERE id = ?', (stripped, color, survivor_id)) + + # The Downloads which apply them (download_manager.yaml is stripped the same way). + downloads = connection.exec_driver_sql( + 'SELECT id, tag_names, settings FROM download WHERE tag_names IS NOT NULL OR settings IS NOT NULL').fetchall() + for id_, tag_names, settings in downloads: + tag_names, settings = json.loads(tag_names) if tag_names else None, json.loads(settings) if settings else None + new_tag_names = _strip_names(tag_names) if isinstance(tag_names, list) else tag_names + new_settings = _strip_nested(settings) + if new_tag_names != tag_names or new_settings != settings: + connection.exec_driver_sql( + 'UPDATE download SET tag_names = ?, settings = ? WHERE id = ?', + (json.dumps(new_tag_names) if new_tag_names is not None else None, + json.dumps(new_settings) if new_settings is not None else None, id_)) + return colors + + +def upgrade(): + colors = strip_tag_names_in_db(op.get_bind()) + strip_tag_names_in_configs(colors) + + +def downgrade(): + # Whitespace that was stripped is not restored. + pass diff --git a/app/cypress/e2e/settings/special-directories.cy.js b/app/cypress/e2e/settings/special-directories.cy.js index 0a9e248d..021df88d 100644 --- a/app/cypress/e2e/settings/special-directories.cy.js +++ b/app/cypress/e2e/settings/special-directories.cy.js @@ -1,5 +1,5 @@ /* - * The five Special Directories fields on /admin/settings. + * The six Special Directories fields on /admin/settings. * * These are the fields that shipped printing their own prefix over their own value, and the * ones whose visible labels were never associated with their inputs. Both faults were @@ -19,6 +19,7 @@ describe('Special Directories settings', () => { ['Map Directory', 'map'], ['Zims Directory', 'zims'], ['Playlists Directory', 'playlists'], + ['Repos Directory', 'repos/%(repo_tag)s/%(repo_name)s'], ]; beforeEach(() => { @@ -38,8 +39,8 @@ describe('Special Directories settings', () => { it('names every directory field, so they can be told apart', () => { /* - * All five fields sit in one grid, hold similar-looking relative paths, and share the - * same prefix. Without a name attached to the input, a screen reader user hears five + * All six fields sit in one grid, hold similar-looking relative paths, and share the + * same prefix. Without a name attached to the input, a screen reader user hears six * fields described only as "/media/wrolpi/" and cannot tell which one saves videos. */ directories.forEach(([label, value]) => { diff --git a/app/cypress/support/component.js b/app/cypress/support/component.js index e6d426e8..fc66e133 100644 --- a/app/cypress/support/component.js +++ b/app/cypress/support/component.js @@ -41,7 +41,7 @@ import React from "react"; Cypress.on('uncaught:exception', (err, runnable) => { if (err.message.includes('ChunkLoadError')) { - return false; // Ignore this error which happens after CircleCI tests. + return false; // Ignore this error which happens after CI tests. } // Otherwise, fail like normal return true; diff --git a/app/package-lock.json b/app/package-lock.json index 0ff57ad5..94743961 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -14,34 +14,36 @@ "@mantine/core": "^8.3.18", "@mantine/hooks": "^8.3.18", "@mantine/notifications": "^8.3.18", - "@tabler/icons-react": "^3.46.0", + "@tabler/icons-react": "^3.49.0", "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^14.6.6", + "@testing-library/user-event": "^14.6.7", "cypress": "^15.21.1", - "esptool-js": "^0.6.1", + "dompurify": "^3.4.17", + "esptool-js": "^0.7.0", "lodash": "^4.18.1", "maplibre-gl": "^5.24.0", + "markdown-it": "^15.0.3", "mathjs": "^15.2.0", "pmtiles": "^4.5.0", "protomaps-themes-base": "^4.5.0", "query-string": "^9.5.1", - "react": "^18.2.0", - "react-colorful": "^5.8.0", - "react-dom": "^18.2.0", - "react-dropzone": "^14.2.3", + "react": "^18.3.1", + "react-colorful": "^5.8.1", + "react-dom": "^18.3.1", + "react-dropzone": "^14.4.1", "react-hotkeys-hook": "^5.3.3", "react-qr-code": "^2.2.0", - "react-router": "^7.18.2", + "react-router": "^7.18.4", "react-scripts": "5.0.1", "react-stl-viewer": "^2.5.0", - "three": "^0.185", - "web-vitals": "^5.3.0" + "three": "^0.186", + "web-vitals": "^6.2.3" }, "devDependencies": { - "@testing-library/dom": "^10.4.1", + "@testing-library/dom": "^10.4.2", "@testing-library/jest-dom": "^7.0.1", "@types/lodash": "^4.17.25", - "@types/node": "^26.4.0", + "@types/node": "^26.6.4", "@types/react": "^18.3.17", "@types/react-dom": "^18.3.5", "typescript": "^5.9.3", @@ -3710,9 +3712,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "funding": [ { "type": "github", @@ -5975,9 +5977,9 @@ } }, "node_modules/@tabler/icons": { - "version": "3.46.0", - "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.46.0.tgz", - "integrity": "sha512-f2RYFl3fzPwj5WO82x6en0dmkjefxEfOm16D1ByM6cj/McNiwOkL4VaPUoP9VVIrXAD9WnTSVFr70px703b//A==", + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.49.0.tgz", + "integrity": "sha512-l+GnSkFETx0U26icxv1VUFnxxiTqXfCcVRiRAnyMwibupM2S2L/hSaZ9gmSgz01rkrsH/93NvLsxykqdanc6lw==", "license": "MIT", "funding": { "type": "github", @@ -5985,12 +5987,12 @@ } }, "node_modules/@tabler/icons-react": { - "version": "3.46.0", - "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.46.0.tgz", - "integrity": "sha512-CCm7xJWhDT2PH4ZIFkP6AgYKtVhq0gpYkjUN+GVh1AzmIQaa77OW0bQPBPQiTE0PsXMR9oSxFqA3qBglzPyrVQ==", + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.49.0.tgz", + "integrity": "sha512-Um59pm5RVNUaQjFUtG0r4oYJbZuBS3tGugAA2tgBg0WufMk34Eio3nsO30RUkLuL2giYPQUNdY9HvqltLLYyNA==", "license": "MIT", "dependencies": { - "@tabler/icons": "3.46.0" + "@tabler/icons": "3.49.0" }, "funding": { "type": "github", @@ -6001,9 +6003,9 @@ } }, "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz", + "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.10.4", @@ -6163,9 +6165,9 @@ } }, "node_modules/@testing-library/user-event": { - "version": "14.6.6", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.6.tgz", - "integrity": "sha512-Jbs9FpkkIDw8FgSc6kOVsOv8JuuqGAL7J4X1oot77JxAoDlkNn2GRkd0aYRVuQ+pVQAiHWVkE4rX/dkF5fBiCw==", + "version": "14.6.7", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.7.tgz", + "integrity": "sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==", "license": "MIT", "engines": { "node": ">=12", @@ -6466,12 +6468,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.4.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz", - "integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==", + "version": "26.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", + "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", "license": "MIT", "dependencies": { - "undici-types": "~8.3.0" + "undici-types": "~8.9.0" } }, "node_modules/@types/node-forge": { @@ -7483,9 +7485,10 @@ "license": "MIT" }, "node_modules/attr-accept": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", - "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", + "license": "MIT", "engines": { "node": ">=4" } @@ -8075,9 +8078,10 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" }, "node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -8627,9 +8631,10 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.10.0.tgz", + "integrity": "sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==", + "license": "MIT" }, "node_modules/colorette": { "version": "2.0.20", @@ -8701,13 +8706,15 @@ } }, "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.2.tgz", + "integrity": "sha512-o8vI5RE5A6EVVOd9o41jKp41aJom+QTEO/Bx8MYNjexMo/Bv2WOjUfZr+aL0WnYSgymUy6zeguqLTsIhV0gMvQ==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", + "destroy": "1.2.0", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", @@ -8715,6 +8722,10 @@ }, "engines": { "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/compression/node_modules/debug": { @@ -9926,6 +9937,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz", + "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", @@ -11008,14 +11028,13 @@ } }, "node_modules/esptool-js": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/esptool-js/-/esptool-js-0.6.1.tgz", - "integrity": "sha512-WNgQTfaEIgHyEiT56pI5v7Tq6Pzjc2XaibLxAtWY4v3zE2Ofk5ImkJY5foEUr0JrdkfHWf6rNizAewN4/kSpHw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/esptool-js/-/esptool-js-0.7.0.tgz", + "integrity": "sha512-9wHuW/s9t4Vs6HKcr3pra3gbRVWwaN0eabMWodWH4KEJHEYZxgmWwZ3s0F4xcWVCZOc/L98bGZ4uWoZvR8KBKA==", "license": "Apache-2.0", "dependencies": { "atob-lite": "^2.0.0", - "pako": "^2.1.0", - "tslib": "^2.4.1" + "pako": "^2.1.0" } }, "node_modules/esquery": { @@ -11144,9 +11163,9 @@ } }, "node_modules/express": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", - "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz", + "integrity": "sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -11168,9 +11187,9 @@ "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", + "path-to-regexp": "~0.1.13", "proxy-addr": "~2.0.7", - "qs": "~6.15.1", + "qs": "~6.16.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", @@ -11207,6 +11226,22 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/express/node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -11262,9 +11297,9 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, "node_modules/fast-uri": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", - "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "funding": [ { "type": "github", @@ -11341,11 +11376,12 @@ } }, "node_modules/file-selector": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", - "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", + "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", + "license": "MIT", "dependencies": { - "tslib": "^2.4.0" + "tslib": "^2.7.0" }, "engines": { "node": ">= 12" @@ -11361,9 +11397,10 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -11489,15 +11526,16 @@ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==" }, "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz", + "integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -15275,9 +15313,9 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", - "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -15484,13 +15522,13 @@ } }, "node_modules/launch-editor": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", - "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.2.tgz", + "integrity": "sha512-NHJEb30MGBCkmZXLZwz25t37QR4aY03ZzPYUJXOJaOa92uWwExI9SBQUuIznBYc1xkD7cJNGX7oc3Nl3sq2kQQ==", "license": "MIT", "dependencies": { "picocolors": "^1.1.1", - "shell-quote": "^1.8.4" + "shell-quote": "^1.10.0" } }, "node_modules/leven": { @@ -15526,6 +15564,25 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "node_modules/linkify-it": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz", + "integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^3.0.0" + } + }, "node_modules/listr2": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", @@ -16021,6 +16078,61 @@ "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", "license": "ISC" }, + "node_modules/markdown-it": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.3.tgz", + "integrity": "sha512-+Az7Ojz3iRGu8KYMvJ6xVhvrxP3eE+vEtJX75Ow88XBpcQNV/0lGX5ZynIIBftREo4DC2jsveAMDaeltmm8QvQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^3.0.3", + "entities": "^8.1.0", + "linkify-it": "^6.1.0", + "mdurl": "^2.1.0", + "punycode.js": "^2.3.1", + "uc.micro": "^3.0.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.3.tgz", + "integrity": "sha512-Sn98ftV3KfsO8xfLPHG6oT8y+b0yvbrLgZjgPDXsharNY15tTAjqIsIju4S32nl6/iEUNNE4v+yL2Zhm8ooP3w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "PSF-2.0" + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -16068,6 +16180,12 @@ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==" }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "license": "MIT" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -16319,9 +16437,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", "funding": [ { "type": "github", @@ -18369,15 +18487,20 @@ "license": "BSD-3-Clause" }, "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "license": "MIT", "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/proxy-addr/node_modules/ipaddr.js": { @@ -18415,6 +18538,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pvtsutils": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", @@ -18602,9 +18734,10 @@ } }, "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" }, @@ -18629,9 +18762,10 @@ } }, "node_modules/react-colorful": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.8.0.tgz", - "integrity": "sha512-Wy9OzPfjSN9bF12OB8N7UQvlsZ0I+7wHxpN+bV5BjNQGxOj6IiwkRjevJK9yOBjJWGQvAaf1OXtn8rUeEatAng==", + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.8.1.tgz", + "integrity": "sha512-oz68bhsnFWnpDf1ZR8daiQbYpXUnM2h2J6hl9Zg2rTpM/DU6vCqe1E+CpqmqLnJucMZetHZeifSAfJ+geN9lcA==", + "license": "MIT", "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" @@ -18755,24 +18889,26 @@ } }, "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.3.1" } }, "node_modules/react-dropzone": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", - "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==", + "version": "14.4.1", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.4.1.tgz", + "integrity": "sha512-QDuV76v3uKbHiH34SpwifZ+gOLi1+RdsCO1kl5vxMT4wW8R82+sthjvBw4th3NHF/XX6FBsqDYZVNN+pnhaw0g==", + "license": "MIT", "dependencies": { - "attr-accept": "^2.2.2", - "file-selector": "^0.6.0", + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", "prop-types": "^15.8.1" }, "engines": { @@ -18902,9 +19038,10 @@ } }, "node_modules/react-router": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", - "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz", + "integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==", + "license": "MIT", "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" @@ -19882,9 +20019,10 @@ } }, "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" } @@ -20149,9 +20287,9 @@ } }, "node_modules/shell-quote": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", - "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", + "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -20312,9 +20450,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -21146,9 +21284,10 @@ } }, "node_modules/three": { - "version": "0.185.1", - "resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz", - "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==" + "version": "0.186.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.186.1.tgz", + "integrity": "sha512-blFeqb49wRCSGUGj7gtpfnSGHy2lwDk94RhUmS1c/hTby70kvChbWpkJ4Pm1390LqzzvTmzgXKHPEafJwCb8jA==", + "license": "MIT" }, "node_modules/three-stdlib": { "version": "2.17.2", @@ -21491,6 +21630,12 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz", + "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==", + "license": "MIT" + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -21506,9 +21651,10 @@ } }, "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==" + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -21860,9 +22006,10 @@ } }, "node_modules/web-vitals": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.3.0.tgz", - "integrity": "sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==" + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-6.2.3.tgz", + "integrity": "sha512-Dmy+vdO1UPMqXiAtZSNayhl4f794Bxfd94W4HIj2HlYtczEQfEOon969memlcpcHrTuOKLa5baFjX7WbgmOq+A==", + "license": "Apache-2.0" }, "node_modules/webidl-conversions": { "version": "6.1.0", @@ -25511,9 +25658,9 @@ } }, "js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "requires": { "argparse": "^2.0.1" } @@ -27020,22 +27167,22 @@ } }, "@tabler/icons": { - "version": "3.46.0", - "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.46.0.tgz", - "integrity": "sha512-f2RYFl3fzPwj5WO82x6en0dmkjefxEfOm16D1ByM6cj/McNiwOkL4VaPUoP9VVIrXAD9WnTSVFr70px703b//A==" + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.49.0.tgz", + "integrity": "sha512-l+GnSkFETx0U26icxv1VUFnxxiTqXfCcVRiRAnyMwibupM2S2L/hSaZ9gmSgz01rkrsH/93NvLsxykqdanc6lw==" }, "@tabler/icons-react": { - "version": "3.46.0", - "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.46.0.tgz", - "integrity": "sha512-CCm7xJWhDT2PH4ZIFkP6AgYKtVhq0gpYkjUN+GVh1AzmIQaa77OW0bQPBPQiTE0PsXMR9oSxFqA3qBglzPyrVQ==", + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.49.0.tgz", + "integrity": "sha512-Um59pm5RVNUaQjFUtG0r4oYJbZuBS3tGugAA2tgBg0WufMk34Eio3nsO30RUkLuL2giYPQUNdY9HvqltLLYyNA==", "requires": { - "@tabler/icons": "3.46.0" + "@tabler/icons": "3.49.0" } }, "@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz", + "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", "requires": { "@babel/code-frame": "7.29.0", "@babel/runtime": "^7.12.5", @@ -27148,9 +27295,9 @@ } }, "@testing-library/user-event": { - "version": "14.6.6", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.6.tgz", - "integrity": "sha512-Jbs9FpkkIDw8FgSc6kOVsOv8JuuqGAL7J4X1oot77JxAoDlkNn2GRkd0aYRVuQ+pVQAiHWVkE4rX/dkF5fBiCw==", + "version": "14.6.7", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.7.tgz", + "integrity": "sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==", "requires": {} }, "@tootallnate/once": { @@ -27412,11 +27559,11 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, "@types/node": { - "version": "26.4.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz", - "integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==", + "version": "26.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", + "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", "requires": { - "undici-types": "~8.3.0" + "undici-types": "~8.9.0" } }, "@types/node-forge": { @@ -28177,9 +28324,9 @@ "integrity": "sha512-LEeSAWeh2Gfa2FtlQE1shxQ8zi5F9GHarrGKz08TMdODD5T4eH6BMsvtnhbWZ+XQn+Gb6om/917ucvRu7l7ukw==" }, "attr-accept": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", - "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==" + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==" }, "autoprefixer": { "version": "10.4.13", @@ -28601,9 +28748,9 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" }, "brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -28971,9 +29118,9 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.10.0.tgz", + "integrity": "sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==" }, "colorette": { "version": "2.0.20", @@ -29023,13 +29170,14 @@ } }, "compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.2.tgz", + "integrity": "sha512-o8vI5RE5A6EVVOd9o41jKp41aJom+QTEO/Bx8MYNjexMo/Bv2WOjUfZr+aL0WnYSgymUy6zeguqLTsIhV0gMvQ==", "requires": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", + "destroy": "1.2.0", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", @@ -29878,6 +30026,14 @@ "domelementtype": "^2.2.0" } }, + "dompurify": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz", + "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==", + "requires": { + "@types/trusted-types": "^2.0.7" + } + }, "domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", @@ -30661,13 +30817,12 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esptool-js": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/esptool-js/-/esptool-js-0.6.1.tgz", - "integrity": "sha512-WNgQTfaEIgHyEiT56pI5v7Tq6Pzjc2XaibLxAtWY4v3zE2Ofk5ImkJY5foEUr0JrdkfHWf6rNizAewN4/kSpHw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/esptool-js/-/esptool-js-0.7.0.tgz", + "integrity": "sha512-9wHuW/s9t4Vs6HKcr3pra3gbRVWwaN0eabMWodWH4KEJHEYZxgmWwZ3s0F4xcWVCZOc/L98bGZ4uWoZvR8KBKA==", "requires": { "atob-lite": "^2.0.0", - "pako": "^2.1.0", - "tslib": "^2.4.1" + "pako": "^2.1.0" } }, "esquery": { @@ -30762,9 +30917,9 @@ } }, "express": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", - "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz", + "integrity": "sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -30785,9 +30940,9 @@ "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", + "path-to-regexp": "~0.1.13", "proxy-addr": "~2.0.7", - "qs": "~6.15.1", + "qs": "~6.16.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", @@ -30816,6 +30971,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "requires": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + } } } }, @@ -30867,9 +31031,9 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, "fast-uri": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", - "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==" + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==" }, "fastq": { "version": "1.15.0", @@ -30918,11 +31082,11 @@ } }, "file-selector": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", - "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", + "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", "requires": { - "tslib": "^2.4.0" + "tslib": "^2.7.0" } }, "filelist": { @@ -30934,9 +31098,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "requires": { "balanced-match": "^1.0.0" } @@ -31032,9 +31196,9 @@ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==" }, "follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==" + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz", + "integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==" }, "for-each": { "version": "0.3.3", @@ -33681,9 +33845,9 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", - "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -33841,12 +34005,12 @@ } }, "launch-editor": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", - "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.2.tgz", + "integrity": "sha512-NHJEb30MGBCkmZXLZwz25t37QR4aY03ZzPYUJXOJaOa92uWwExI9SBQUuIznBYc1xkD7cJNGX7oc3Nl3sq2kQQ==", "requires": { "picocolors": "^1.1.1", - "shell-quote": "^1.8.4" + "shell-quote": "^1.10.0" } }, "leven": { @@ -33873,6 +34037,14 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "linkify-it": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz", + "integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==", + "requires": { + "uc.micro": "^3.0.0" + } + }, "listr2": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", @@ -34223,6 +34395,31 @@ } } }, + "markdown-it": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.3.tgz", + "integrity": "sha512-+Az7Ojz3iRGu8KYMvJ6xVhvrxP3eE+vEtJX75Ow88XBpcQNV/0lGX5ZynIIBftREo4DC2jsveAMDaeltmm8QvQ==", + "requires": { + "argparse": "^3.0.3", + "entities": "^8.1.0", + "linkify-it": "^6.1.0", + "mdurl": "^2.1.0", + "punycode.js": "^2.3.1", + "uc.micro": "^3.0.0" + }, + "dependencies": { + "argparse": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.3.tgz", + "integrity": "sha512-Sn98ftV3KfsO8xfLPHG6oT8y+b0yvbrLgZjgPDXsharNY15tTAjqIsIju4S32nl6/iEUNNE4v+yL2Zhm8ooP3w==" + }, + "entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==" + } + } + }, "math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -34256,6 +34453,11 @@ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==" }, + "mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==" + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -34428,9 +34630,9 @@ "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==" }, "nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==" + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==" }, "natural-compare": { "version": "1.4.0", @@ -35700,9 +35902,9 @@ "integrity": "sha512-z5A6oXAXNi/CHEIbCfzZ/6uVDgnv3pO4C3LFvomxjUFXwyqWZ7HJdfPoNJSIlsvaQubqsaxCbamEzqBr/W7vkg==" }, "proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", "requires": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -35739,6 +35941,11 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" }, + "punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==" + }, "pvtsutils": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", @@ -35863,9 +36070,9 @@ } }, "react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "requires": { "loose-envify": "^1.1.0" } @@ -35884,9 +36091,9 @@ } }, "react-colorful": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.8.0.tgz", - "integrity": "sha512-Wy9OzPfjSN9bF12OB8N7UQvlsZ0I+7wHxpN+bV5BjNQGxOj6IiwkRjevJK9yOBjJWGQvAaf1OXtn8rUeEatAng==", + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.8.1.tgz", + "integrity": "sha512-oz68bhsnFWnpDf1ZR8daiQbYpXUnM2h2J6hl9Zg2rTpM/DU6vCqe1E+CpqmqLnJucMZetHZeifSAfJ+geN9lcA==", "requires": {} }, "react-dev-utils": { @@ -35976,21 +36183,21 @@ } }, "react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "requires": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.23.2" } }, "react-dropzone": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", - "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==", + "version": "14.4.1", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.4.1.tgz", + "integrity": "sha512-QDuV76v3uKbHiH34SpwifZ+gOLi1+RdsCO1kl5vxMT4wW8R82+sthjvBw4th3NHF/XX6FBsqDYZVNN+pnhaw0g==", "requires": { - "attr-accept": "^2.2.2", - "file-selector": "^0.6.0", + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", "prop-types": "^15.8.1" } }, @@ -36071,9 +36278,9 @@ } }, "react-router": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", - "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz", + "integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==", "requires": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" @@ -36707,9 +36914,9 @@ } }, "scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "requires": { "loose-envify": "^1.1.0" } @@ -36930,9 +37137,9 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, "shell-quote": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", - "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==" + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", + "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==" }, "side-channel": { "version": "1.1.1", @@ -37038,9 +37245,9 @@ "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==" }, "source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==" }, "source-map-loader": { "version": "3.0.2", @@ -37608,9 +37815,9 @@ "requires": {} }, "three": { - "version": "0.185.1", - "resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz", - "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==" + "version": "0.186.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.186.1.tgz", + "integrity": "sha512-blFeqb49wRCSGUGj7gtpfnSGHy2lwDk94RhUmS1c/hTby70kvChbWpkJ4Pm1390LqzzvTmzgXKHPEafJwCb8jA==" }, "three-stdlib": { "version": "2.17.2", @@ -37870,6 +38077,11 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==" }, + "uc.micro": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz", + "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==" + }, "unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -37882,9 +38094,9 @@ } }, "undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==" + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==" }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -38111,9 +38323,9 @@ } }, "web-vitals": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.3.0.tgz", - "integrity": "sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==" + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-6.2.3.tgz", + "integrity": "sha512-Dmy+vdO1UPMqXiAtZSNayhl4f794Bxfd94W4HIj2HlYtczEQfEOon969memlcpcHrTuOKLa5baFjX7WbgmOq+A==" }, "webidl-conversions": { "version": "6.1.0", diff --git a/app/package.json b/app/package.json index aadddfc5..8dd5dd31 100644 --- a/app/package.json +++ b/app/package.json @@ -9,28 +9,30 @@ "@mantine/core": "^8.3.18", "@mantine/hooks": "^8.3.18", "@mantine/notifications": "^8.3.18", - "@tabler/icons-react": "^3.46.0", + "@tabler/icons-react": "^3.49.0", "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^14.6.6", + "@testing-library/user-event": "^14.6.7", "cypress": "^15.21.1", - "esptool-js": "^0.6.1", + "dompurify": "^3.4.17", + "esptool-js": "^0.7.0", "lodash": "^4.18.1", "maplibre-gl": "^5.24.0", + "markdown-it": "^15.0.3", "mathjs": "^15.2.0", "pmtiles": "^4.5.0", "protomaps-themes-base": "^4.5.0", "query-string": "^9.5.1", - "react": "^18.2.0", - "react-colorful": "^5.8.0", - "react-dom": "^18.2.0", - "react-dropzone": "^14.2.3", + "react": "^18.3.1", + "react-colorful": "^5.8.1", + "react-dom": "^18.3.1", + "react-dropzone": "^14.4.1", "react-hotkeys-hook": "^5.3.3", "react-qr-code": "^2.2.0", - "react-router": "^7.18.2", + "react-router": "^7.18.4", "react-scripts": "5.0.1", "react-stl-viewer": "^2.5.0", - "three": "^0.185", - "web-vitals": "^5.3.0" + "three": "^0.186", + "web-vitals": "^6.2.3" }, "scripts": { "start": "react-scripts start", @@ -58,10 +60,10 @@ ] }, "devDependencies": { - "@testing-library/dom": "^10.4.1", + "@testing-library/dom": "^10.4.2", "@testing-library/jest-dom": "^7.0.1", "@types/lodash": "^4.17.25", - "@types/node": "^26.4.0", + "@types/node": "^26.6.4", "@types/react": "^18.3.17", "@types/react-dom": "^18.3.5", "typescript": "^5.9.3", diff --git a/app/public/data/solar-climatology.LICENSE.txt b/app/public/data/solar-climatology.LICENSE.txt new file mode 100644 index 00000000..4290e795 --- /dev/null +++ b/app/public/data/solar-climatology.LICENSE.txt @@ -0,0 +1,11 @@ +Solar climatology for the WROLPi Solar calculator + +Source: NASA POWER, https://power.larc.nasa.gov/ +Data: 2001-2020 monthly climatology of ALLSKY_SFC_SW_DWN, ALLSKY_SFC_SW_DIFF, ALLSKY_SRF_ALB, T2M, on a 1° grid. +The data was obtained from the POWER Project's POWER Climatology API v2.10.0 version on 2026/10/04. + +The data was obtained from National Aeronautics and Space Administration (NASA) Langley Research Center's Prediction Of Worldwide Energy Resources (POWER) project funded through the NASA Earth Science Division. + +License: Creative Commons Attribution 4.0 International (CC BY 4.0), https://creativecommons.org/licenses/by/4.0/ + +Changes: WROLPi modified the data: values are rounded to fit one byte each (0.04 kWh/m²/day for irradiance, 0.004 for albedo, 0.5 °C for temperature), temperatures are averaged from NASA's 0.5° x 0.625° grid into 1° cells, and the calculator interpolates between cells. diff --git a/app/public/data/solar-climatology.bin b/app/public/data/solar-climatology.bin new file mode 100644 index 00000000..9a5bcb68 Binary files /dev/null and b/app/public/data/solar-climatology.bin differ diff --git a/app/public/epub/epub.html b/app/public/epub/epub.html index 3a1ab60e..73f1289a 100644 --- a/app/public/epub/epub.html +++ b/app/public/epub/epub.html @@ -242,6 +242,8 @@ either expressed or implied, of the FreeBSD Project. --> // Optional search term passed in from a search-result deep link. If present we'll // search the spine item at `loc` and jump to + highlight the first match. let searchQuery = (params && params.get("q")) ? params.get("q") : null; + // Where the reader left off (an EPUB CFI). A search result's `loc` wins over it. + let startCfi = (params && params.get("cfi")) ? params.get("cfi") : null; let book = ePub(url); let rendition = book.renderTo("viewer", { @@ -250,7 +252,33 @@ either expressed or implied, of the FreeBSD Project. --> height: "100%" }); - rendition.display(currentSectionIndex); + rendition.display(currentSectionIndex !== undefined ? currentSectionIndex : (startCfi || undefined)); + + // How far through the book `location` is, from its chapter and its page within that chapter. Cheaper + // than `book.locations.generate()`, which stalls a Raspberry Pi on a large book. + function readingProgress(location) { + if (location.atEnd) { + return 1; + } + const start = location.start; + const sections = book.spine.length || 1; + const displayed = start.displayed || {page: 1, total: 1}; + const withinSection = (displayed.page - 1) / Math.max(displayed.total, 1); + return Math.min((start.index + withinSection) / sections, 1); + } + + // Tell the WROLPi page that embeds this viewer where the reader is, so it can save their place. Nothing is + // posted at the start of the book: that is where a finished book opens, and saving it would un-finish it. + function postLocation(location) { + if (window.parent === window || !location || !location.start || location.atStart) { + return; + } + window.parent.postMessage({ + type: "wrolpi:epub-location", + cfi: location.start.cfi, + progress: readingProgress(location), + }, window.location.origin); + } // --- In-book search --------------------------------------------------- // State is module-level so the toolbar buttons can drive it. `hits` is the @@ -516,6 +544,7 @@ either expressed or implied, of the FreeBSD Project. --> prev.style.visibility = "visible"; } + postLocation(location); }); rendition.on("layout", function (layout) { diff --git a/app/src/App.css b/app/src/App.css index d0f46346..6b423219 100644 --- a/app/src/App.css +++ b/app/src/App.css @@ -316,6 +316,90 @@ pre.wrap-text { height: 100%; } +/* A markdown file preview, formatted or raw. Scrolls rather than fitting like .preview-fit, because + it is a document to read, not media to frame. Colors come from the theme tokens. */ +.markdown-preview { + height: calc(100dvh - 21rem); + min-height: 15rem; + overflow: auto; + margin-top: 0.5em; + padding: 0 0.25em; +} + +/* A repo's README is part of its page, not a modal; it grows with its content. */ +.repo-readme .markdown-preview { + height: auto; + min-height: 0; + overflow: visible; +} + +.markdown-raw { + margin: 0; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.markdown-body { + max-width: 50rem; + line-height: 1.6; + overflow-wrap: break-word; +} + +.markdown-body > :first-child { + margin-top: 0; +} + +.markdown-body h1, .markdown-body h2 { + padding-bottom: 0.25em; + border-bottom: 1px solid var(--table-line); +} + +.markdown-body img { + max-width: 100%; +} + +.markdown-body blockquote { + margin: 0 0 1em; + padding: 0 1em; + color: var(--muted); + border-left: 0.25em solid var(--border); +} + +.markdown-body code { + padding: 0.1em 0.3em; + border-radius: 0.25em; + background-color: var(--head); +} + +.markdown-body pre { + padding: 0.75em 1em; + overflow-x: auto; + border-radius: 0.25em; + background-color: var(--head); +} + +.markdown-body pre code { + padding: 0; + background-color: transparent; +} + +.markdown-body table { + display: block; + max-width: 100%; + overflow-x: auto; + border-collapse: collapse; +} + +.markdown-body th, .markdown-body td { + padding: 0.3em 0.75em; + border: 1px solid var(--table-line); +} + +.markdown-body hr { + border: none; + border-top: 1px solid var(--border); +} + td.file-path { cursor: pointer; } @@ -725,3 +809,35 @@ tr.drag-deselecting td { display: block; } } + +/* Map popups (POI details and pins) follow the active theme's tokens. */ +/* Scoped under .maplibregl-popup to outrank maplibre-gl.css, which loads after this file. */ +.maplibregl-popup .maplibregl-popup-content { + background: var(--panel); + color: var(--text); + border: 1px solid var(--border); + border-radius: 0.25rem; +} +.maplibregl-popup .maplibregl-popup-close-button { + color: var(--muted); +} +.maplibregl-popup .maplibregl-popup-close-button:hover { + background: var(--head); + color: var(--text); +} +.maplibregl-popup.maplibregl-popup-anchor-top .maplibregl-popup-tip, +.maplibregl-popup.maplibregl-popup-anchor-top-left .maplibregl-popup-tip, +.maplibregl-popup.maplibregl-popup-anchor-top-right .maplibregl-popup-tip { + border-bottom-color: var(--panel); +} +.maplibregl-popup.maplibregl-popup-anchor-bottom .maplibregl-popup-tip, +.maplibregl-popup.maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip, +.maplibregl-popup.maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip { + border-top-color: var(--panel); +} +.maplibregl-popup.maplibregl-popup-anchor-left .maplibregl-popup-tip { + border-right-color: var(--panel); +} +.maplibregl-popup.maplibregl-popup-anchor-right .maplibregl-popup-tip { + border-left-color: var(--panel); +} diff --git a/app/src/App.js b/app/src/App.js index 1e372a53..088cfa95 100644 --- a/app/src/App.js +++ b/app/src/App.js @@ -17,6 +17,7 @@ import {ArchiveRoute} from "./components/Archive"; import {FilesRoute} from "./components/Files"; import {QueryProvider, StatusProvider} from "./hooks/customHooks"; import {FileWorkerStatusProvider} from "./contexts/FileWorkerStatusContext"; +import {BookmarksProvider} from "./contexts/BookmarksContext"; import {ChannelEditPage, ChannelNewPage, ChannelsPage} from "./components/Channels"; import {MapRoute} from "./components/Map"; import {MediaContextProvider, mediaStyles, StatusContext} from "./contexts/contexts"; @@ -29,6 +30,7 @@ import {FilePreviewProvider} from "./components/FilePreview"; import {TagsProvider} from "./Tags"; import {ZimRoute} from "./components/Zim"; import {PlaylistsRoute} from "./components/Playlists"; +import {ReposRoute} from "./components/Repos"; import {DocsRoute} from "./components/Docs"; import {FlasherRoute} from "./components/Flasher"; import ErrorBoundary from "./components/ErrorBoundary"; @@ -69,13 +71,15 @@ function Root() { -
- -
- - - -