diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..f79823f0 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,253 @@ +# 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 29480843..e1969755 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,11 +7,6 @@ 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: @@ -42,42 +37,6 @@ 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 deleted file mode 100644 index 05cec023..00000000 --- a/.github/workflows/backend.yml +++ /dev/null @@ -1,145 +0,0 @@ -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 deleted file mode 100644 index efb01e97..00000000 --- a/.github/workflows/cypress.yml +++ /dev/null @@ -1,121 +0,0 @@ -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 a3ab9bc5..655c907f 100644 --- a/.gitignore +++ b/.gitignore @@ -150,6 +150,3 @@ 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 cf93f460..1fc77ee8 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -3,10 +3,8 @@ 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 13 Install](#debian-13-install) — install WROLPi onto an x86 PC or laptop. +* [Debian 12 Install](#debian-12-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. @@ -38,7 +36,6 @@ 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: @@ -55,50 +52,30 @@ 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 13 Install +## Debian 12 Install -Install WROLPi onto the internal disk of an x86 PC or laptop. This uses the same -`WROLPi-v*-amd64.iso` as WROLPi Portable. +Steps necessary to initialize your WROLPi after installing the Debian image from wrolpi.org -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). +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! ## Raspberry Pi Install -Install WROLPi onto a Raspberry Pi. An external drive is recommended for your library. +Steps necessary to initialize your WROLPi after installing the Raspberry Pi image from wrolpi.org -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. +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! diff --git a/README.md b/README.md index 291d2507..dfe580fd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # WROLPi -[![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) +[![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) [![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 13](INSTALL.md#debian-13-install)** — install WROLPi onto an x86 PC or laptop. +* **[Debian 12](INSTALL.md#debian-12-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 7029bb37..0b3449af 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 Installation](#upgrading-raspberry-pi-or-debian-installation) +* [Upgrading Raspberry Pi or Debian 12 Installation](#upgrading-raspberry-pi-or-debian-12-installation) * [Upgrading a WROLPi Portable USB](#upgrading-a-wrolpi-portable-usb) ## Upgrading Docker containers @@ -24,9 +24,7 @@ 7. Start all docker containers * `docker-compose up -d` -## 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. +## Upgrading Raspberry Pi or Debian 12 Installation 1. Run the upgrade script * `sudo /bin/bash /opt/wrolpi/upgrade.sh` diff --git a/alembic/env.py b/alembic/env.py index 1fc7bd9c..9aeeb116 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -6,10 +6,9 @@ from alembic import context # access to the values within the .ini file in use. config = context.config -# 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) +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) # add your model's MetaData object here # for 'autogenerate' support @@ -24,7 +23,6 @@ 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 @@ -98,10 +96,7 @@ def run_migrations_online(): connectable = create_wrolpi_engine(get_url()) - # 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: + with connectable.connect() 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 deleted file mode 100644 index 405dbfe1..00000000 --- a/alembic/versions/2026_09_27_1200_domain_collection_summary.py +++ /dev/null @@ -1,45 +0,0 @@ -"""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 deleted file mode 100644 index d8ca602b..00000000 --- a/alembic/versions/2026_09_28_1200_collection_listing_indexes.py +++ /dev/null @@ -1,29 +0,0 @@ -"""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 deleted file mode 100644 index 02300ea4..00000000 --- a/alembic/versions/2026_10_05_1200_markdown_mimetype.py +++ /dev/null @@ -1,31 +0,0 @@ -"""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 deleted file mode 100644 index e4537267..00000000 --- a/alembic/versions/2026_10_06_1200_repository.py +++ /dev/null @@ -1,43 +0,0 @@ -"""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 deleted file mode 100644 index b4fd7abc..00000000 --- a/alembic/versions/2026_10_06_1300_repository_search.py +++ /dev/null @@ -1,39 +0,0 @@ -"""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 deleted file mode 100644 index 123ba97d..00000000 --- a/alembic/versions/2026_10_06_1400_repository_submodules.py +++ /dev/null @@ -1,22 +0,0 @@ -"""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 deleted file mode 100644 index 61d936af..00000000 --- a/alembic/versions/2026_10_06_1500_repository_clone_token.py +++ /dev/null @@ -1,22 +0,0 @@ -"""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 deleted file mode 100644 index 5742beee..00000000 --- a/alembic/versions/2026_10_08_1200_file_group_progress.py +++ /dev/null @@ -1,24 +0,0 @@ -"""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 deleted file mode 100644 index bf202b50..00000000 --- a/alembic/versions/2026_10_24_1200_config_version.py +++ /dev/null @@ -1,27 +0,0 @@ -"""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 deleted file mode 100644 index a076bbf8..00000000 --- a/alembic/versions/2026_10_25_1200_strip_tag_names.py +++ /dev/null @@ -1,175 +0,0 @@ -"""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 021df88d..0a9e248d 100644 --- a/app/cypress/e2e/settings/special-directories.cy.js +++ b/app/cypress/e2e/settings/special-directories.cy.js @@ -1,5 +1,5 @@ /* - * The six Special Directories fields on /admin/settings. + * The five 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,7 +19,6 @@ describe('Special Directories settings', () => { ['Map Directory', 'map'], ['Zims Directory', 'zims'], ['Playlists Directory', 'playlists'], - ['Repos Directory', 'repos/%(repo_tag)s/%(repo_name)s'], ]; beforeEach(() => { @@ -39,8 +38,8 @@ describe('Special Directories settings', () => { it('names every directory field, so they can be told apart', () => { /* - * 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 + * 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 * 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 fc66e133..e6d426e8 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 CI tests. + return false; // Ignore this error which happens after CircleCI tests. } // Otherwise, fail like normal return true; diff --git a/app/package-lock.json b/app/package-lock.json index 94743961..0ff57ad5 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -14,36 +14,34 @@ "@mantine/core": "^8.3.18", "@mantine/hooks": "^8.3.18", "@mantine/notifications": "^8.3.18", - "@tabler/icons-react": "^3.49.0", + "@tabler/icons-react": "^3.46.0", "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^14.6.7", + "@testing-library/user-event": "^14.6.6", "cypress": "^15.21.1", - "dompurify": "^3.4.17", - "esptool-js": "^0.7.0", + "esptool-js": "^0.6.1", "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.3.1", - "react-colorful": "^5.8.1", - "react-dom": "^18.3.1", - "react-dropzone": "^14.4.1", + "react": "^18.2.0", + "react-colorful": "^5.8.0", + "react-dom": "^18.2.0", + "react-dropzone": "^14.2.3", "react-hotkeys-hook": "^5.3.3", "react-qr-code": "^2.2.0", - "react-router": "^7.18.4", + "react-router": "^7.18.2", "react-scripts": "5.0.1", "react-stl-viewer": "^2.5.0", - "three": "^0.186", - "web-vitals": "^6.2.3" + "three": "^0.185", + "web-vitals": "^5.3.0" }, "devDependencies": { - "@testing-library/dom": "^10.4.2", + "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^7.0.1", "@types/lodash": "^4.17.25", - "@types/node": "^26.6.4", + "@types/node": "^26.4.0", "@types/react": "^18.3.17", "@types/react-dom": "^18.3.5", "typescript": "^5.9.3", @@ -3712,9 +3710,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "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==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "funding": [ { "type": "github", @@ -5977,9 +5975,9 @@ } }, "node_modules/@tabler/icons": { - "version": "3.49.0", - "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.49.0.tgz", - "integrity": "sha512-l+GnSkFETx0U26icxv1VUFnxxiTqXfCcVRiRAnyMwibupM2S2L/hSaZ9gmSgz01rkrsH/93NvLsxykqdanc6lw==", + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.46.0.tgz", + "integrity": "sha512-f2RYFl3fzPwj5WO82x6en0dmkjefxEfOm16D1ByM6cj/McNiwOkL4VaPUoP9VVIrXAD9WnTSVFr70px703b//A==", "license": "MIT", "funding": { "type": "github", @@ -5987,12 +5985,12 @@ } }, "node_modules/@tabler/icons-react": { - "version": "3.49.0", - "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.49.0.tgz", - "integrity": "sha512-Um59pm5RVNUaQjFUtG0r4oYJbZuBS3tGugAA2tgBg0WufMk34Eio3nsO30RUkLuL2giYPQUNdY9HvqltLLYyNA==", + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.46.0.tgz", + "integrity": "sha512-CCm7xJWhDT2PH4ZIFkP6AgYKtVhq0gpYkjUN+GVh1AzmIQaa77OW0bQPBPQiTE0PsXMR9oSxFqA3qBglzPyrVQ==", "license": "MIT", "dependencies": { - "@tabler/icons": "3.49.0" + "@tabler/icons": "3.46.0" }, "funding": { "type": "github", @@ -6003,9 +6001,9 @@ } }, "node_modules/@testing-library/dom": { - "version": "10.4.2", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz", - "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.10.4", @@ -6165,9 +6163,9 @@ } }, "node_modules/@testing-library/user-event": { - "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==", + "version": "14.6.6", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.6.tgz", + "integrity": "sha512-Jbs9FpkkIDw8FgSc6kOVsOv8JuuqGAL7J4X1oot77JxAoDlkNn2GRkd0aYRVuQ+pVQAiHWVkE4rX/dkF5fBiCw==", "license": "MIT", "engines": { "node": ">=12", @@ -6468,12 +6466,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.6.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", - "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", + "version": "26.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz", + "integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==", "license": "MIT", "dependencies": { - "undici-types": "~8.9.0" + "undici-types": "~8.3.0" } }, "node_modules/@types/node-forge": { @@ -7485,10 +7483,9 @@ "license": "MIT" }, "node_modules/attr-accept": { - "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", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", + "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==", "engines": { "node": ">=4" } @@ -8078,10 +8075,9 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" }, "node_modules/brace-expansion": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", - "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", - "license": "MIT", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -8631,10 +8627,9 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/colord": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.10.0.tgz", - "integrity": "sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==", - "license": "MIT" + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" }, "node_modules/colorette": { "version": "2.0.20", @@ -8706,15 +8701,13 @@ } }, "node_modules/compression": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.2.tgz", - "integrity": "sha512-o8vI5RE5A6EVVOd9o41jKp41aJom+QTEO/Bx8MYNjexMo/Bv2WOjUfZr+aL0WnYSgymUy6zeguqLTsIhV0gMvQ==", - "license": "MIT", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "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", @@ -8722,10 +8715,6 @@ }, "engines": { "node": ">= 0.8.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" } }, "node_modules/compression/node_modules/debug": { @@ -9937,15 +9926,6 @@ "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", @@ -11028,13 +11008,14 @@ } }, "node_modules/esptool-js": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/esptool-js/-/esptool-js-0.7.0.tgz", - "integrity": "sha512-9wHuW/s9t4Vs6HKcr3pra3gbRVWwaN0eabMWodWH4KEJHEYZxgmWwZ3s0F4xcWVCZOc/L98bGZ4uWoZvR8KBKA==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/esptool-js/-/esptool-js-0.6.1.tgz", + "integrity": "sha512-WNgQTfaEIgHyEiT56pI5v7Tq6Pzjc2XaibLxAtWY4v3zE2Ofk5ImkJY5foEUr0JrdkfHWf6rNizAewN4/kSpHw==", "license": "Apache-2.0", "dependencies": { "atob-lite": "^2.0.0", - "pako": "^2.1.0" + "pako": "^2.1.0", + "tslib": "^2.4.1" } }, "node_modules/esquery": { @@ -11163,9 +11144,9 @@ } }, "node_modules/express": { - "version": "4.22.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz", - "integrity": "sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -11187,9 +11168,9 @@ "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.13", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "~6.16.0", + "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", @@ -11226,22 +11207,6 @@ "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", @@ -11297,9 +11262,9 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, "node_modules/fast-uri": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", - "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", "funding": [ { "type": "github", @@ -11376,12 +11341,11 @@ } }, "node_modules/file-selector": { - "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", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", + "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", "dependencies": { - "tslib": "^2.7.0" + "tslib": "^2.4.0" }, "engines": { "node": ">= 12" @@ -11397,10 +11361,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "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", + "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==", "dependencies": { "balanced-match": "^1.0.0" } @@ -11526,16 +11489,15 @@ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==" }, "node_modules/follow-redirects": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz", - "integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==", + "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==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], - "license": "MIT", "engines": { "node": ">=4.0" }, @@ -15313,9 +15275,9 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { - "version": "3.15.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", - "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -15522,13 +15484,13 @@ } }, "node_modules/launch-editor": { - "version": "2.14.2", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.2.tgz", - "integrity": "sha512-NHJEb30MGBCkmZXLZwz25t37QR4aY03ZzPYUJXOJaOa92uWwExI9SBQUuIznBYc1xkD7cJNGX7oc3Nl3sq2kQQ==", + "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==", "license": "MIT", "dependencies": { "picocolors": "^1.1.1", - "shell-quote": "^1.10.0" + "shell-quote": "^1.8.4" } }, "node_modules/leven": { @@ -15564,25 +15526,6 @@ "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", @@ -16078,61 +16021,6 @@ "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", @@ -16180,12 +16068,6 @@ "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", @@ -16437,9 +16319,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.19", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", - "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", @@ -18487,20 +18369,15 @@ "license": "BSD-3-Clause" }, "node_modules/proxy-addr": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", - "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", - "license": "MIT", + "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==", "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": { @@ -18538,15 +18415,6 @@ "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", @@ -18734,10 +18602,9 @@ } }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "dependencies": { "loose-envify": "^1.1.0" }, @@ -18762,10 +18629,9 @@ } }, "node_modules/react-colorful": { - "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", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.8.0.tgz", + "integrity": "sha512-Wy9OzPfjSN9bF12OB8N7UQvlsZ0I+7wHxpN+bV5BjNQGxOj6IiwkRjevJK9yOBjJWGQvAaf1OXtn8rUeEatAng==", "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" @@ -18889,26 +18755,24 @@ } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", "dependencies": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.23.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^18.2.0" } }, "node_modules/react-dropzone": { - "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", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", + "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==", "dependencies": { - "attr-accept": "^2.2.4", - "file-selector": "^2.1.0", + "attr-accept": "^2.2.2", + "file-selector": "^0.6.0", "prop-types": "^15.8.1" }, "engines": { @@ -19038,10 +18902,9 @@ } }, "node_modules/react-router": { - "version": "7.18.4", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz", - "integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==", - "license": "MIT", + "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==", "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" @@ -20019,10 +19882,9 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", "dependencies": { "loose-envify": "^1.1.0" } @@ -20287,9 +20149,9 @@ } }, "node_modules/shell-quote": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", - "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -20450,9 +20312,9 @@ } }, "node_modules/source-map-js": { - "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==", + "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==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -21284,10 +21146,9 @@ } }, "node_modules/three": { - "version": "0.186.1", - "resolved": "https://registry.npmjs.org/three/-/three-0.186.1.tgz", - "integrity": "sha512-blFeqb49wRCSGUGj7gtpfnSGHy2lwDk94RhUmS1c/hTby70kvChbWpkJ4Pm1390LqzzvTmzgXKHPEafJwCb8jA==", - "license": "MIT" + "version": "0.185.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz", + "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==" }, "node_modules/three-stdlib": { "version": "2.17.2", @@ -21630,12 +21491,6 @@ "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", @@ -21651,10 +21506,9 @@ } }, "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", - "license": "MIT" + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -22006,10 +21860,9 @@ } }, "node_modules/web-vitals": { - "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" + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.3.0.tgz", + "integrity": "sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==" }, "node_modules/webidl-conversions": { "version": "6.1.0", @@ -25658,9 +25511,9 @@ } }, "js-yaml": { - "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==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "requires": { "argparse": "^2.0.1" } @@ -27167,22 +27020,22 @@ } }, "@tabler/icons": { - "version": "3.49.0", - "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.49.0.tgz", - "integrity": "sha512-l+GnSkFETx0U26icxv1VUFnxxiTqXfCcVRiRAnyMwibupM2S2L/hSaZ9gmSgz01rkrsH/93NvLsxykqdanc6lw==" + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.46.0.tgz", + "integrity": "sha512-f2RYFl3fzPwj5WO82x6en0dmkjefxEfOm16D1ByM6cj/McNiwOkL4VaPUoP9VVIrXAD9WnTSVFr70px703b//A==" }, "@tabler/icons-react": { - "version": "3.49.0", - "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.49.0.tgz", - "integrity": "sha512-Um59pm5RVNUaQjFUtG0r4oYJbZuBS3tGugAA2tgBg0WufMk34Eio3nsO30RUkLuL2giYPQUNdY9HvqltLLYyNA==", + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.46.0.tgz", + "integrity": "sha512-CCm7xJWhDT2PH4ZIFkP6AgYKtVhq0gpYkjUN+GVh1AzmIQaa77OW0bQPBPQiTE0PsXMR9oSxFqA3qBglzPyrVQ==", "requires": { - "@tabler/icons": "3.49.0" + "@tabler/icons": "3.46.0" } }, "@testing-library/dom": { - "version": "10.4.2", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz", - "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "requires": { "@babel/code-frame": "7.29.0", "@babel/runtime": "^7.12.5", @@ -27295,9 +27148,9 @@ } }, "@testing-library/user-event": { - "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==", + "version": "14.6.6", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.6.tgz", + "integrity": "sha512-Jbs9FpkkIDw8FgSc6kOVsOv8JuuqGAL7J4X1oot77JxAoDlkNn2GRkd0aYRVuQ+pVQAiHWVkE4rX/dkF5fBiCw==", "requires": {} }, "@tootallnate/once": { @@ -27559,11 +27412,11 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, "@types/node": { - "version": "26.6.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", - "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", + "version": "26.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz", + "integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==", "requires": { - "undici-types": "~8.9.0" + "undici-types": "~8.3.0" } }, "@types/node-forge": { @@ -28324,9 +28177,9 @@ "integrity": "sha512-LEeSAWeh2Gfa2FtlQE1shxQ8zi5F9GHarrGKz08TMdODD5T4eH6BMsvtnhbWZ+XQn+Gb6om/917ucvRu7l7ukw==" }, "attr-accept": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", - "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==" + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", + "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==" }, "autoprefixer": { "version": "10.4.13", @@ -28748,9 +28601,9 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" }, "brace-expansion": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", - "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -29118,9 +28971,9 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "colord": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.10.0.tgz", - "integrity": "sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==" + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" }, "colorette": { "version": "2.0.20", @@ -29170,14 +29023,13 @@ } }, "compression": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.2.tgz", - "integrity": "sha512-o8vI5RE5A6EVVOd9o41jKp41aJom+QTEO/Bx8MYNjexMo/Bv2WOjUfZr+aL0WnYSgymUy6zeguqLTsIhV0gMvQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "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", @@ -30026,14 +29878,6 @@ "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", @@ -30817,12 +30661,13 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esptool-js": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/esptool-js/-/esptool-js-0.7.0.tgz", - "integrity": "sha512-9wHuW/s9t4Vs6HKcr3pra3gbRVWwaN0eabMWodWH4KEJHEYZxgmWwZ3s0F4xcWVCZOc/L98bGZ4uWoZvR8KBKA==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/esptool-js/-/esptool-js-0.6.1.tgz", + "integrity": "sha512-WNgQTfaEIgHyEiT56pI5v7Tq6Pzjc2XaibLxAtWY4v3zE2Ofk5ImkJY5foEUr0JrdkfHWf6rNizAewN4/kSpHw==", "requires": { "atob-lite": "^2.0.0", - "pako": "^2.1.0" + "pako": "^2.1.0", + "tslib": "^2.4.1" } }, "esquery": { @@ -30917,9 +30762,9 @@ } }, "express": { - "version": "4.22.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz", - "integrity": "sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -30940,9 +30785,9 @@ "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.13", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "~6.16.0", + "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", @@ -30971,15 +30816,6 @@ "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" - } } } }, @@ -31031,9 +30867,9 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, "fast-uri": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", - "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==" + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==" }, "fastq": { "version": "1.15.0", @@ -31082,11 +30918,11 @@ } }, "file-selector": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", - "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", + "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", "requires": { - "tslib": "^2.7.0" + "tslib": "^2.4.0" } }, "filelist": { @@ -31098,9 +30934,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", - "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "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==", "requires": { "balanced-match": "^1.0.0" } @@ -31196,9 +31032,9 @@ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==" }, "follow-redirects": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz", - "integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==" + "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==" }, "for-each": { "version": "0.3.3", @@ -33845,9 +33681,9 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { - "version": "3.15.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", - "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -34005,12 +33841,12 @@ } }, "launch-editor": { - "version": "2.14.2", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.2.tgz", - "integrity": "sha512-NHJEb30MGBCkmZXLZwz25t37QR4aY03ZzPYUJXOJaOa92uWwExI9SBQUuIznBYc1xkD7cJNGX7oc3Nl3sq2kQQ==", + "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==", "requires": { "picocolors": "^1.1.1", - "shell-quote": "^1.10.0" + "shell-quote": "^1.8.4" } }, "leven": { @@ -34037,14 +33873,6 @@ "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", @@ -34395,31 +34223,6 @@ } } }, - "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", @@ -34453,11 +34256,6 @@ "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", @@ -34630,9 +34428,9 @@ "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==" }, "nanoid": { - "version": "3.3.19", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", - "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==" + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==" }, "natural-compare": { "version": "1.4.0", @@ -35902,9 +35700,9 @@ "integrity": "sha512-z5A6oXAXNi/CHEIbCfzZ/6uVDgnv3pO4C3LFvomxjUFXwyqWZ7HJdfPoNJSIlsvaQubqsaxCbamEzqBr/W7vkg==" }, "proxy-addr": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", - "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "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==", "requires": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -35941,11 +35739,6 @@ "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", @@ -36070,9 +35863,9 @@ } }, "react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "requires": { "loose-envify": "^1.1.0" } @@ -36091,9 +35884,9 @@ } }, "react-colorful": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.8.1.tgz", - "integrity": "sha512-oz68bhsnFWnpDf1ZR8daiQbYpXUnM2h2J6hl9Zg2rTpM/DU6vCqe1E+CpqmqLnJucMZetHZeifSAfJ+geN9lcA==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.8.0.tgz", + "integrity": "sha512-Wy9OzPfjSN9bF12OB8N7UQvlsZ0I+7wHxpN+bV5BjNQGxOj6IiwkRjevJK9yOBjJWGQvAaf1OXtn8rUeEatAng==", "requires": {} }, "react-dev-utils": { @@ -36183,21 +35976,21 @@ } }, "react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", "requires": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.23.0" } }, "react-dropzone": { - "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==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", + "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==", "requires": { - "attr-accept": "^2.2.4", - "file-selector": "^2.1.0", + "attr-accept": "^2.2.2", + "file-selector": "^0.6.0", "prop-types": "^15.8.1" } }, @@ -36278,9 +36071,9 @@ } }, "react-router": { - "version": "7.18.4", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz", - "integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==", + "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==", "requires": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" @@ -36914,9 +36707,9 @@ } }, "scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", "requires": { "loose-envify": "^1.1.0" } @@ -37137,9 +36930,9 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, "shell-quote": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", - "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==" + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==" }, "side-channel": { "version": "1.1.1", @@ -37245,9 +37038,9 @@ "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==" }, "source-map-js": { - "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==" + "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==" }, "source-map-loader": { "version": "3.0.2", @@ -37815,9 +37608,9 @@ "requires": {} }, "three": { - "version": "0.186.1", - "resolved": "https://registry.npmjs.org/three/-/three-0.186.1.tgz", - "integrity": "sha512-blFeqb49wRCSGUGj7gtpfnSGHy2lwDk94RhUmS1c/hTby70kvChbWpkJ4Pm1390LqzzvTmzgXKHPEafJwCb8jA==" + "version": "0.185.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz", + "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==" }, "three-stdlib": { "version": "2.17.2", @@ -38077,11 +37870,6 @@ "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", @@ -38094,9 +37882,9 @@ } }, "undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==" + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==" }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -38323,9 +38111,9 @@ } }, "web-vitals": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-6.2.3.tgz", - "integrity": "sha512-Dmy+vdO1UPMqXiAtZSNayhl4f794Bxfd94W4HIj2HlYtczEQfEOon969memlcpcHrTuOKLa5baFjX7WbgmOq+A==" + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.3.0.tgz", + "integrity": "sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==" }, "webidl-conversions": { "version": "6.1.0", diff --git a/app/package.json b/app/package.json index 8dd5dd31..aadddfc5 100644 --- a/app/package.json +++ b/app/package.json @@ -9,30 +9,28 @@ "@mantine/core": "^8.3.18", "@mantine/hooks": "^8.3.18", "@mantine/notifications": "^8.3.18", - "@tabler/icons-react": "^3.49.0", + "@tabler/icons-react": "^3.46.0", "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^14.6.7", + "@testing-library/user-event": "^14.6.6", "cypress": "^15.21.1", - "dompurify": "^3.4.17", - "esptool-js": "^0.7.0", + "esptool-js": "^0.6.1", "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.3.1", - "react-colorful": "^5.8.1", - "react-dom": "^18.3.1", - "react-dropzone": "^14.4.1", + "react": "^18.2.0", + "react-colorful": "^5.8.0", + "react-dom": "^18.2.0", + "react-dropzone": "^14.2.3", "react-hotkeys-hook": "^5.3.3", "react-qr-code": "^2.2.0", - "react-router": "^7.18.4", + "react-router": "^7.18.2", "react-scripts": "5.0.1", "react-stl-viewer": "^2.5.0", - "three": "^0.186", - "web-vitals": "^6.2.3" + "three": "^0.185", + "web-vitals": "^5.3.0" }, "scripts": { "start": "react-scripts start", @@ -60,10 +58,10 @@ ] }, "devDependencies": { - "@testing-library/dom": "^10.4.2", + "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^7.0.1", "@types/lodash": "^4.17.25", - "@types/node": "^26.6.4", + "@types/node": "^26.4.0", "@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 deleted file mode 100644 index 4290e795..00000000 --- a/app/public/data/solar-climatology.LICENSE.txt +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 9a5bcb68..00000000 Binary files a/app/public/data/solar-climatology.bin and /dev/null differ diff --git a/app/public/epub/epub.html b/app/public/epub/epub.html index 73f1289a..3a1ab60e 100644 --- a/app/public/epub/epub.html +++ b/app/public/epub/epub.html @@ -242,8 +242,6 @@ 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", { @@ -252,33 +250,7 @@ either expressed or implied, of the FreeBSD Project. --> height: "100%" }); - 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); - } + rendition.display(currentSectionIndex); // --- In-book search --------------------------------------------------- // State is module-level so the toolbar buttons can drive it. `hits` is the @@ -544,7 +516,6 @@ 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 6b423219..d0f46346 100644 --- a/app/src/App.css +++ b/app/src/App.css @@ -316,90 +316,6 @@ 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; } @@ -809,35 +725,3 @@ 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 088cfa95..a9881881 100644 --- a/app/src/App.js +++ b/app/src/App.js @@ -17,7 +17,6 @@ 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"; @@ -30,9 +29,9 @@ 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 {AiRoute} from "./components/Ai"; import ErrorBoundary from "./components/ErrorBoundary"; import {KeyboardShortcutsProvider} from "./components/KeyboardShortcutsProvider"; @@ -71,15 +70,13 @@ function Root() { - -
- -
- - - -