diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..2ab591e --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,261 @@ +name: CI + +on: + push: + branches: + - "**" + pull_request: + +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + +jobs: + + # ─── 1. Generate OpenAPI types ──────────────────────────────────────────── + + generate-types: + name: Generate TypeScript types + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "22" + + - run: corepack enable + + - run: cd types && yarn install --frozen-lockfile && yarn openapi-ts + + - uses: actions/upload-artifact@v3 + with: + name: types-gen + path: types/gen + + # ─── 2a. Server unit tests ──────────────────────────────────────────────── + + server-test: + name: Server unit tests + runs-on: ubuntu-latest + needs: generate-types + env: + NODE_ENV: test + JWT_SECRET: test-secret-min-32-chars-aaaaaaa! + MOCK_DATA: "true" + STORAGE: json + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "22" + + - run: corepack enable + + - uses: actions/download-artifact@v3 + with: + name: types-gen + path: types/gen + + - run: cd server && yarn install --frozen-lockfile && yarn test + + # ─── 2b. Build server ───────────────────────────────────────────────────── + + server-build: + name: Build server + runs-on: ubuntu-latest + needs: generate-types + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "22" + + - run: corepack enable + + - uses: actions/download-artifact@v3 + with: + name: types-gen + path: types/gen + + - run: cd types && yarn install --frozen-lockfile + + - run: cd server && yarn install --frozen-lockfile && yarn build + + - uses: actions/upload-artifact@v3 + with: + name: server-dist + path: server/dist + + # ─── 2c. Build client ───────────────────────────────────────────────────── + + client-build: + name: Build client + runs-on: ubuntu-latest + needs: generate-types + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "22" + + - run: corepack enable + + - uses: actions/download-artifact@v3 + with: + name: types-gen + path: types/gen + + - run: cd types && yarn install --frozen-lockfile + + - run: cd client && yarn install --frozen-lockfile && yarn build + + - uses: actions/upload-artifact@v3 + with: + name: client-dist + path: client/dist + + # ─── 3. Playwright E2E tests ────────────────────────────────────────────── + + e2e: + name: Playwright E2E tests + runs-on: ubuntu-latest + needs: [ server-build, client-build ] + container: mcr.microsoft.com/playwright:v1.59.1-jammy + services: + redis: + image: redis/redis-stack-server:7.4.0-v1 + env: + REDIS_ARGS: "--save '' --loglevel warning" + env: + CI: "true" + NODE_ENV: test + JWT_SECRET: test-secret-min-32-chars-aaaaaaa! + MOCK_DATA: "true" + STORAGE: redis + REDIS_HOST: redis + REDIS_PORT: "6379" + HTTP_REMOTE_USER_ENABLED: "true" + HTTP_REMOTE_USER_HEADER_NAME: remote-user + HTTP_REMOTE_TRUSTED_IPS: "127.0.0.1,::1,::ffff:127.0.0.1" + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v3 + with: + name: server-dist + path: server/dist + + - uses: actions/download-artifact@v3 + with: + name: client-dist + path: client/dist + + - name: Install server dependencies + run: cd server && yarn install --frozen-lockfile + + - name: Copy client build into server/public + run: cp -r client/dist server/public + + - name: Install e2e dependencies and browsers + run: cd e2e && yarn install --frozen-lockfile && yarn playwright install firefox + --with-deps + + - name: Run Playwright tests + run: cd e2e && yarn test + + - uses: actions/upload-artifact@v3 + if: failure() + with: + name: playwright-report + path: | + e2e/playwright-report + e2e/test-results + + # ─── 4. Build and push Docker image (master only) ───────────────────────── + + docker-build: + name: Build and push Docker image + runs-on: ubuntu-latest + needs: [ server-build, client-build, server-test, e2e ] + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "22" + + - run: corepack enable + + - uses: actions/download-artifact@v3 + with: + name: server-dist + path: server/dist + + - uses: actions/download-artifact@v3 + with: + name: client-dist + path: client/dist + + - name: Install server production dependencies + run: cd server && yarn install --frozen-lockfile --production + + - uses: docker/setup-buildx-action@v3 + + - uses: docker/login-action@v3 + with: + registry: ${{ secrets.REPO_URL }} + username: ${{ secrets.REPO_USERNAME }} + password: ${{ secrets.REPO_PASSWORD }} + + - uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile + target: runner-prebuilt + platforms: linux/amd64 + push: true + tags: ${{ secrets.REPO_URL }}/${{ secrets.REPO_NAME }}:latest + + # ─── 5. Notifications ──────────────────────── + + notify: + name: Notify + runs-on: ubuntu-latest + needs: [ server-build, client-build, server-test, e2e, docker-build ] + if: always() && github.event_name == 'push' + steps: + - name: Send webhook + env: + DISCORD_WEBHOOK_ID: ${{ secrets.DISCORD_WEBHOOK_ID }} + DISCORD_WEBHOOK_TOKEN: ${{ secrets.DISCORD_WEBHOOK_TOKEN }} + NTFY_URL: ${{ secrets.NTFY_URL }} + BUILD_RESULT: ${{ needs.docker-build.result }} + RUN_NUMBER: ${{ github.run_number }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ + github.run_id }} + COMMIT_MESSAGE: ${{ github.event.head_commit.message }} + COMMIT_AUTHOR: ${{ github.event.head_commit.author.name }} + run: | + if [ "$BUILD_RESULT" = "success" ]; then + MSG="✅ Sestavení #${RUN_NUMBER} proběhlo úspěšně." + NTFY_TAGS="white_check_mark" + else + MSG="❌ Sestavení #${RUN_NUMBER} selhalo." + NTFY_TAGS="x" + fi + FULL_MSG="$(printf '%s\n\nPipeline: %s\nPoslední commit: %sAutor: %s' \ + "$MSG" "$RUN_URL" "$COMMIT_MESSAGE" "$COMMIT_AUTHOR")" + curl -s -X POST \ + "https://discord.com/api/webhooks/${DISCORD_WEBHOOK_ID}/${DISCORD_WEBHOOK_TOKEN}" \ + -H "Content-Type: application/json" \ + --data "$(jq -n --arg content "$FULL_MSG" '{content: $content}')" + curl -s -X POST "${NTFY_URL}" \ + -H "Title: Luncher CI #${RUN_NUMBER}" \ + -H "Tags: ${NTFY_TAGS}" \ + -H "Click: ${RUN_URL}" \ + -d "${FULL_MSG}" diff --git a/.gitignore b/.gitignore index 90dc6fd..34dc9f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ node_modules types/gen -**.DS_Store \ No newline at end of file +**.DS_Store +.mcp.json +.claude/settings.local.json +server/public/ diff --git a/.woodpecker/workflow.yaml b/.woodpecker/workflow.yaml deleted file mode 100644 index bfbff41..0000000 --- a/.woodpecker/workflow.yaml +++ /dev/null @@ -1,64 +0,0 @@ -variables: - - &node_image "node:22-alpine" - - &branch "master" - -when: - - event: push - branch: *branch - -steps: - - name: Generate TypeScript types - image: *node_image - commands: - - cd types - - yarn install --frozen-lockfile - - yarn openapi-ts - - name: Install server dependencies - image: *node_image - commands: - - cd server - - yarn install --frozen-lockfile - depends_on: [Generate TypeScript types] - - name: Install client dependencies - image: *node_image - commands: - - cd client - - yarn install --frozen-lockfile - depends_on: [Generate TypeScript types] - - name: Build server - depends_on: [Install server dependencies] - image: *node_image - commands: - - cd server - - yarn build - - name: Build client - depends_on: [Install client dependencies] - image: *node_image - commands: - - cd client - - yarn build - - name: Build Docker image - depends_on: [Build server, Build client] - image: woodpeckerci/plugin-docker-buildx - settings: - dockerfile: Dockerfile-Woodpecker - platforms: linux/amd64 - registry: - from_secret: REPO_URL - username: - from_secret: REPO_USERNAME - password: - from_secret: REPO_PASSWORD - repo: - from_secret: REPO_NAME - - name: Discord notification - build - image: appleboy/drone-discord - depends_on: [Build Docker image] - when: - - status: [success, failure] - settings: - webhook_id: - from_secret: DISCORD_WEBHOOK_ID - webhook_token: - from_secret: DISCORD_WEBHOOK_TOKEN - message: "{{#success build.status}}✅ Sestavení {{build.number}} proběhlo úspěšně.{{else}}❌ Sestavení {{build.number}} selhalo.{{/success}}\n\nPipeline: {{build.link}}\nPoslední commit: {{commit.message}}Autor: {{commit.author}}" diff --git a/CLAUDE.md b/CLAUDE.md index 3d03f91..c7736cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,9 +12,12 @@ Luncher is a lunch management app for teams — daily restaurant menus, food ord types/ → Shared OpenAPI-generated TypeScript types (source of truth: types/api.yml) server/ → Express 5 backend (Node.js 22, ts-node) client/ → React 19 frontend (Vite 7, React Bootstrap) +e2e/ → Playwright E2E tests (separate package) ``` -Each directory has its own `package.json` and `tsconfig.json`. Package manager: **Yarn Classic**. +Each of the four directories has its own `package.json`. Package manager: **Yarn Classic**. + +Deployment files at repo root: `Dockerfile` (multi-stage, dva runner targety: výchozí `runner` pro lokální build, `runner-prebuilt` pro CI s předem sestavenými artefakty), `compose.yml`, `compose-traefik.yml`. ## Development Commands @@ -23,6 +26,7 @@ Each directory has its own `package.json` and `tsconfig.json`. Package manager: cd types && yarn install && yarn openapi-ts # Generate API types first cd ../server && yarn install cd ../client && yarn install +cd ../e2e && yarn install ``` ### Running dev environment @@ -44,38 +48,62 @@ cd client && yarn build # tsc --noEmit + vite build → client/dist ### Tests ```bash -cd server && yarn test # Jest (tests in server/src/tests/) +# Server unit tests (Jest) +cd server && yarn test # All tests in server/src/tests/ +cd server && yarn test dates # Run one file by name +cd server && yarn test -t "name" # Run by test name pattern + +# E2E (Playwright) — requires prebuilt server +cd server && yarn build +cd e2e && yarn test # chromium + firefox, baseURL 127.0.0.1:3001 +cd e2e && yarn test:ui # interactive UI mode +cd e2e && yarn report # open last HTML report ``` +Jest setup (`server/src/tests/setupEnv.ts`) forces `STORAGE=memory`, deletes `MOCK_DATA`, and sets a fixed `JWT_SECRET`. Playwright auto-starts the prebuilt server and authenticates via the `remote-user: e2e-user` trusted-header path; locally uses `STORAGE=json` + `MOCK_DATA=true`, CI uses `STORAGE=redis`. + +### CI pipeline +Gitea Actions — `.gitea/workflows/ci.yaml` (no `.github/` equivalent): + +1. `generate-types` — runs `yarn openapi-ts`, uploads artifact +2. `server-test` — Jest +3. `server-build` + `client-build` — parallel tsc/vite builds +4. `e2e` — Playwright in `mcr.microsoft.com/playwright:v1.59.1-jammy` with a Redis service container; only Firefox installed in CI +5. `docker-build` — master branch only, uses `Dockerfile` with `--target runner-prebuilt` (skládá image z artefaktů `server-build` + `client-build`) +6. `notify` — Discord + ntfy webhooks + ### Formatting -```bash -# Prettier available in client (no config file — uses defaults) -``` +Prettier is installed in `client/` (devDependency only, no script or config) — invoke via `yarn prettier ` with defaults. ## Architecture ### API Types (types/) - OpenAPI 3.0 spec in `types/api.yml` — all API endpoints and DTOs defined here +- `api.yml` is a thin aggregator — actual endpoint specs live in `types/paths//*.yml`, shared schemas in `types/schemas/_index.yml` - `yarn openapi-ts` generates `types/gen/` (client.gen.ts, sdk.gen.ts, types.gen.ts) - Both server and client import from these generated types - **When changing API contracts: update api.yml first, then regenerate** ### Server (server/src/) - **Entry:** `index.ts` — Express app + Socket.io setup -- **Routes:** `routes/` — modular Express route handlers (food, pizzaDay, voting, notifications, qr, stats, easterEgg, dev) +- **Routes:** `routes/` — 9 modular Express route handlers (food, pizzaDay, voting, notifications, qr, stats, easterEgg, dev, changelog) - **Services:** domain logic files at root level (`service.ts`, `pizza.ts`, `restaurants.ts`, `chefie.ts`, `voting.ts`, `stats.ts`, `qr.ts`, `notifikace.ts`) +- **Helpers:** `mock.ts` (fake menu data for `MOCK_DATA=true`), `pushReminder.ts` (push notification reminders), `utils.ts` (shared utilities) - **Auth:** `auth.ts` — JWT + optional trusted-header authentication -- **Storage:** `storage/StorageInterface.ts` defines the interface; implementations in `storage/json.ts` (file-based, dev) and `storage/redis.ts` (production). Data keyed by date (YYYY-MM-DD). +- **Storage:** `storage/index.ts` factory selects implementation; backends: `json.ts` (file-based, dev), `redis.ts` (production), `memory.ts` (tests). Data keyed by date (YYYY-MM-DD). - **Restaurant scrapers:** Cheerio-based HTML parsing for daily menus from multiple restaurants +- **Pizza Chefie integration:** `chefie.ts` scrapes pizzachefie.cz for Pizza day menus and salads (only active when a Pizza day is open) - **WebSocket:** `websocket.ts` — Socket.io for real-time client updates - **Mock mode:** `MOCK_DATA=true` env var returns fake menus (useful for weekend/holiday dev) - **Config:** `.env.development` / `.env.production` (see `.env.template` for all options) ### Client (client/src/) -- **Entry:** `index.tsx` → `App.tsx` → `AppRoutes.tsx` +- **Entry:** `index.tsx` → `App.tsx` → `AppRoutes.tsx`; `Login.tsx` is the auth screen; `FallingLeaves.tsx` is a seasonal visual effect - **Pages:** `pages/` (StatsPage) -- **Components:** `components/` (Header, Footer, Loader, modals/, PizzaOrderList, PizzaOrderRow) -- **Context providers:** `context/` — AuthContext, SettingsContext, SocketContext, EasterEggContext +- **Components:** `components/` (Header, Footer, Loader, PizzaOrderList, PizzaOrderRow, and modals in `components/modals/`) +- **Context providers:** `context/` — `auth.tsx`, `settings.tsx`, `socket.js`, `eggs.tsx` (note: `socket.js` is the only non-TSX context file) +- **Hooks:** `hooks/` (`usePushReminder.ts`) +- **Utils:** `utils/` (`parsePrice.ts`) - **Styling:** Bootstrap 5 + React Bootstrap + custom SCSS files (co-located with components) - **API calls:** use OpenAPI-generated SDK from `types/gen/` - **Routing:** React Router DOM v7 @@ -89,7 +117,7 @@ cd server && yarn test # Jest (tests in server/src/tests/) ## Environment - **Server env files:** `server/.env.development`, `server/.env.production` (see `server/.env.template`) -- Key vars: `JWT_SECRET`, `STORAGE` (json/redis), `MOCK_DATA`, `REDIS_HOST`, `REDIS_PORT` +- Key vars: `JWT_SECRET`, `STORAGE` (json/redis/memory), `MOCK_DATA`, `REDIS_HOST`, `REDIS_PORT` - **Docker:** multi-stage Dockerfile, `compose.yml` for app + Redis. Timezone: Europe/Prague. ## Conventions @@ -97,3 +125,4 @@ cd server && yarn test # Jest (tests in server/src/tests/) - Czech naming for domain variables and UI strings; English for infrastructure code - TypeScript strict mode in both client and server - Server module resolution: Node16; Client: ESNext/bundler +- `TODO.md` tracks open bugs and roadmap items — worth scanning before starting non-trivial work diff --git a/Dockerfile b/Dockerfile index 67bfdc1..5f673bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ ARG NODE_VERSION="node:22-alpine" -# Builder +# ─── Builder ────────────────────────────────────────────────────────────────── FROM ${NODE_VERSION} AS builder WORKDIR /build @@ -62,8 +62,9 @@ RUN yarn build WORKDIR /build/client RUN yarn build -# Runner -FROM ${NODE_VERSION} +# ─── Runner base ────────────────────────────────────────────────────────────── +# Společný základ pro oba runner targety – nastaví prostředí a metadata běhu. +FROM ${NODE_VERSION} AS runner-base RUN apk add --no-cache tzdata ENV TZ=Europe/Prague \ @@ -72,6 +73,17 @@ ENV TZ=Europe/Prague \ WORKDIR /app +# Export /data/db.json do složky /data +VOLUME ["/data"] + +EXPOSE 3000 + +CMD [ "node", "./server/src/index.js" ] + +# ─── Runner (default) ───────────────────────────────────────────────────────── +# Použití: docker build . (lokální sestavení – vše se buildí uvnitř image) +FROM runner-base AS runner + # Vykopírování sestaveného serveru COPY --from=builder /build/server/node_modules ./server/node_modules COPY --from=builder /build/server/dist ./ @@ -82,12 +94,28 @@ COPY --from=builder /build/client/dist ./public # Zkopírování produkčních .env serveru COPY /server/.env.production ./server +# Zkopírování changelogů (seznamu novinek) +COPY /server/changelogs ./server/changelogs + # Zkopírování konfigurace easter eggů -RUN if [ -f /server/.easter-eggs.json ]; then cp /server/.easter-eggs.json ./server/; fi +RUN if [ -f ./server/.easter-eggs.json ]; then cp ./server/.easter-eggs.json ./server/; fi -# Export /data/db.json do složky /data -VOLUME ["/data"] +# ─── Runner (prebuilt) ──────────────────────────────────────────────────────── +# Použití: docker build --target runner-prebuilt . +# Očekává předem sestavené artefakty v build kontextu (server/dist, +# client/dist, server/node_modules) – využívá Gitea Actions, kde se +# server i klient buildí v separátních jobech a sem se jen kopírují. +FROM runner-base AS runner-prebuilt -EXPOSE 3000 +# Vykopírování sestaveného serveru +COPY ./server/node_modules ./server/node_modules +COPY ./server/dist ./ -CMD [ "node", "./server/src/index.js" ] \ No newline at end of file +# Vykopírování sestaveného klienta +COPY ./client/dist ./public + +# Zkopírování changelogů (seznamu novinek) +COPY ./server/changelogs ./server/changelogs + +# Zkopírování konfigurace easter eggů +RUN if [ -f ./server/.easter-eggs.json ]; then cp ./server/.easter-eggs.json ./server/; fi diff --git a/Dockerfile-Woodpecker b/Dockerfile-Woodpecker deleted file mode 100644 index 6291b32..0000000 --- a/Dockerfile-Woodpecker +++ /dev/null @@ -1,26 +0,0 @@ -ARG NODE_VERSION="node:22-alpine" - -FROM ${NODE_VERSION} - -RUN apk add --no-cache tzdata -ENV TZ=Europe/Prague \ - LC_ALL=cs_CZ.UTF-8 \ - NODE_ENV=production - -WORKDIR /app - -# Vykopírování sestaveného serveru -COPY ./server/node_modules ./server/node_modules -COPY ./server/dist ./ -# TODO tohle není dobře, má to být součástí serveru -# COPY ./server/resources ./resources - -# Vykopírování sestaveného klienta -COPY ./client/dist ./public - -# Zkopírování konfigurace easter eggů -RUN if [ -f ./server/.easter-eggs.json ]; then cp ./server/.easter-eggs.json ./server/; fi - -EXPOSE 3000 - -CMD [ "node", "./server/src/index.js" ] diff --git a/client/src/App.tsx b/client/src/App.tsx index e88ac5c..6117bac 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -13,12 +13,13 @@ import './App.scss'; import { faCircleCheck, faNoteSticky, faTrashCan, faComment } from '@fortawesome/free-regular-svg-icons'; import { useSettings } from './context/settings'; import Footer from './components/Footer'; -import { faBasketShopping, faChainBroken, faChevronLeft, faChevronRight, faGear, faSatelliteDish, faSearch, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons'; +import { faBasketShopping, faChainBroken, faChevronLeft, faChevronRight, faGear, faMoneyBillTransfer, faSatelliteDish, faSearch, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons'; import Loader from './components/Loader'; import { getHumanDateTime, isInTheFuture, formatDateString } from './Utils'; import NoteModal from './components/modals/NoteModal'; +import PayForAllModal from './components/modals/PayForAllModal'; import { useEasterEgg } from './context/eggs'; -import { ClientData, Food, MealSlot, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, UserLunchChoice, PizzaVariant, getData, getEasterEggImage, addPizza, removePizza, updatePizzaDayNote, createPizzaDay, deletePizzaDay, lockPizzaDay, unlockPizzaDay, finishOrder, finishDelivery, addChoice, jdemeObed, removeChoices, removeChoice, updateNote, changeDepartureTime, setBuyer, dismissQr } from '../../types'; +import { ClientData, Food, MealSlot, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, LocationLunchChoicesMap, UserLunchChoice, PizzaVariant, getData, getEasterEggImage, addPizza, removePizza, updatePizzaDayNote, createPizzaDay, deletePizzaDay, lockPizzaDay, unlockPizzaDay, finishOrder, finishDelivery, addChoice, jdemeObed, removeChoices, removeChoice, updateNote, changeDepartureTime, setBuyer, dismissQr, generateQr } from '../../types'; import { getLunchChoiceName } from './enums'; // import FallingLeaves, { LEAF_PRESETS, LEAF_COLOR_THEMES } from './FallingLeaves'; // import './FallingLeaves.scss'; @@ -74,6 +75,7 @@ function App() { const [dayIndex, setDayIndex] = useState(); const [loadingPizzaDay, setLoadingPizzaDay] = useState(false); const [noteModalOpen, setNoteModalOpen] = useState(false); + const [payForAllLocationKey, setPayForAllLocationKey] = useState(null); const [eggImage, setEggImage] = useState(); const eggRef = useRef(null); // Prazvláštní workaround, aby socket.io listener viděl aktuální hodnotu @@ -288,6 +290,7 @@ function App() { try { await addChoice({ body: { locationKey: location, foodIndex, dayIndex } }); + await tryAutoSelectDepartureTime(); } catch (error: any) { alert(`Chyba při změně volby: ${error.message || error}`); } @@ -314,6 +317,10 @@ function App() { foodChoiceRef.current.value = ""; } choiceRef.current?.blur(); + // Automatický výběr času odchodu pouze pro restaurace s menu + if (Object.keys(Restaurant).includes(locationKey)) { + await tryAutoSelectDepartureTime(); + } } catch (error: any) { alert(`Chyba při změně volby: ${error.message || error}`); // Reset výběru zpět @@ -338,6 +345,7 @@ function App() { const locationKey = choiceRef.current.value as LunchChoice; if (auth?.login) { await addChoice({ body: { locationKey, foodIndex: Number(event.target.value), dayIndex } }); + await tryAutoSelectDepartureTime(); } } } @@ -386,32 +394,80 @@ function App() { } } + const handleCreatePizzaDay = async () => { + if (!window.confirm('Opravdu chcete založit Pizza day?')) return; + setLoadingPizzaDay(true); + await createPizzaDay().then(() => setLoadingPizzaDay(false)); + } + + const handleDeletePizzaDay = async () => { + if (!window.confirm('Opravdu chcete smazat Pizza day? Budou smazány i všechny dosud zadané objednávky.')) return; + await deletePizzaDay(); + } + + const handleLockPizzaDay = async () => { + if (!window.confirm('Opravdu chcete uzamknout objednávky? Po uzamčení nebude možné přidávat ani odebírat objednávky.')) return; + await lockPizzaDay(); + } + + const handleUnlockPizzaDay = async () => { + if (!window.confirm('Opravdu chcete odemknout objednávky? Uživatelé budou moci opět upravovat své objednávky.')) return; + await unlockPizzaDay(); + } + + const handleFinishOrder = async () => { + if (!window.confirm('Opravdu chcete označit objednávky jako objednané? Objednávky zůstanou zamčeny.')) return; + await finishOrder(); + } + + const handleReturnToLocked = async () => { + if (!window.confirm('Opravdu chcete vrátit stav zpět do "uzamčeno" (před objednáním)?')) return; + await lockPizzaDay(); + } + + const handleFinishDelivery = async () => { + if (!window.confirm(`Opravdu chcete označit pizzy jako doručené?${settings?.bankAccount && settings?.holderName ? ' Uživatelům bude vygenerován QR kód pro platbu.' : ''}`)) return; + await finishDelivery({ body: { bankAccount: settings?.bankAccount, bankAccountHolder: settings?.holderName } }); + } + const pizzaSuggestions = useMemo(() => { - if (!data?.pizzaList) { + if (!data?.pizzaList && !data?.salatList) { return []; } const suggestions: SelectSearchOption[] = []; - data.pizzaList.forEach((pizza, index) => { + data.pizzaList?.forEach((pizza, index) => { const group: SelectSearchOption = { name: pizza.name, type: "group", items: [] } pizza.sizes.forEach((size, sizeIndex) => { const name = `${size.size} (${size.price} Kč)`; - const value = `${index}|${sizeIndex}`; + const value = `pizza|${index}|${sizeIndex}`; group.items?.push({ name, value }); }) suggestions.push(group); - }) + }); + if (data.salatList?.length) { + const salatGroup: SelectSearchOption = { name: "Saláty", type: "group", items: [] } + data.salatList.forEach((salat, index) => { + salatGroup.items?.push({ name: `${salat.name} (${salat.price} Kč)`, value: `salat|${index}` }); + }); + suggestions.push(salatGroup); + } return suggestions; - }, [data?.pizzaList]); + }, [data?.pizzaList, data?.salatList]); const handlePizzaChange = async (value: SelectedOptionValue | SelectedOptionValue[]) => { - if (auth?.login && data?.pizzaList) { + if (auth?.login) { if (typeof value !== 'string') { throw new TypeError('Nepodporovaný typ hodnoty: ' + typeof value); } const s = value.split('|'); - const pizzaIndex = Number.parseInt(s[0]); - const pizzaSizeIndex = Number.parseInt(s[1]); - await addPizza({ body: { pizzaIndex, pizzaSizeIndex } }); + if (s[0] === 'salat') { + const salatIndex = Number.parseInt(s[1]); + await addPizza({ body: { salatIndex } }); + } else { + const pizzaIndex = Number.parseInt(s[1]); + const pizzaSizeIndex = Number.parseInt(s[2]); + await addPizza({ body: { pizzaIndex, pizzaSizeIndex } }); + } } } @@ -433,6 +489,16 @@ function App() { } } + // Automaticky nastaví preferovaný čas odchodu 10:45, pokud tento čas ještě nenastal (nebo jde o budoucí den) + const tryAutoSelectDepartureTime = async () => { + const preferredTime = "10:45" as DepartureTime; + const isToday = dayIndex === data?.todayDayIndex; + if ((!isToday || isInTheFuture(preferredTime)) && departureChoiceRef.current) { + departureChoiceRef.current.value = preferredTime; + await changeDepartureTime({ body: { time: preferredTime, dayIndex } }); + } + } + const handleDayChange = async (dayIndex: number) => { setDayIndex(dayIndex); dayIndexRef.current = dayIndex; @@ -583,7 +649,7 @@ function App() { Je možné vybrat jen jednu možnost. Výběr jiné odstraní předchozí. {foodChoiceList && !closed && <> -

Na co dobrého? (nepovinné)

+

Na co dobrého?

{foodChoiceList.map((food, index) => )} @@ -594,7 +660,7 @@ function App() { {Object.values(DepartureTime) - .filter(time => isInTheFuture(time)) + .filter(time => dayIndex !== data.todayDayIndex || isInTheFuture(time)) .map(time => )} } @@ -616,6 +682,18 @@ function App() { {locationName} {(locationPickCount ?? 0) > 1 && ({locationPickCount})} + {locationPickCount >= 2 && auth.login && loginObject[auth.login] !== undefined + && locationKey !== LunchChoice.PIZZA && locationKey !== LunchChoice.NEOBEDVAM && locationKey !== LunchChoice.ROZHODUJI + && settings?.bankAccount && settings?.holderName && ( + + setPayForAllLocationKey(locationKey)} + className='action-icon' + style={{ cursor: 'pointer' }} + /> + + )} @@ -709,10 +787,7 @@ function App() { :
- +
} @@ -731,12 +806,8 @@ function App() { { data.pizzaDay.creator === auth.login &&
- - + +
} @@ -747,12 +818,8 @@ function App() {

Objednávky jsou uzamčeny uživatelem {data.pizzaDay.creator}

{data.pizzaDay.creator === auth.login &&
- - + +
} @@ -763,12 +830,8 @@ function App() {

Pizzy byly objednány uživatelem {data.pizzaDay.creator}

{data.pizzaDay.creator === auth.login &&
- - + +
} @@ -785,7 +848,7 @@ function App() { { }} onFocus={_ => { }} @@ -808,11 +871,15 @@ function App() { } { - data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr && -
-

QR platba

- QR kód -
+ data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr && (() => { + const pizzaQr = data.pendingQrs?.find(qr => qr.creator === data.pizzaDay?.creator); + return pizzaQr ? ( +
+

QR platba

+ QR kód +
+ ) : null; + })() } } @@ -822,18 +889,17 @@ function App() { {data.pendingQrs && data.pendingQrs.length > 0 &&

Nevyřízené platby

-

Máte neuhrazené platby z předchozích dní.

+

Máte neuhrazené platby.

{data.pendingQrs.map(qr => ( -
+

{formatDateString(qr.date)} — {qr.creator} ({qr.totalPrice} Kč) {qr.purpose && <>
{qr.purpose}}

- QR kód + QR kód
); } diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx index 9ee7b4a..7b0341b 100644 --- a/client/src/components/Header.tsx +++ b/client/src/components/Header.tsx @@ -11,16 +11,12 @@ import GenerateMockDataModal from "./modals/GenerateMockDataModal"; import ClearMockDataModal from "./modals/ClearMockDataModal"; import { useNavigate } from "react-router"; import { STATS_URL, VECERE_URL } from "../AppRoutes"; -import { FeatureRequest, getVotes, updateVote, LunchChoices } from "../../../types"; +import { FeatureRequest, getVotes, updateVote, LunchChoices, getChangelogs } from "../../../types"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons"; +import { formatDateString } from "../Utils"; -const CHANGELOG = [ - "Nový moderní design aplikace", - "Oprava parsování Sladovnické a TechTower", - "Možnost označit se jako objednávající u volby \"budu objednávat\"", - "Možnost generovat QR kódy pro platby (i mimo Pizza day)", -]; +const LAST_SEEN_CHANGELOG_KEY = "lastChangelogDate"; const IS_DEV = process.env.NODE_ENV === 'development'; @@ -38,6 +34,7 @@ export default function Header({ choices, dayIndex }: Props) { const [pizzaModalOpen, setPizzaModalOpen] = useState(false); const [refreshMenuModalOpen, setRefreshMenuModalOpen] = useState(false); const [changelogModalOpen, setChangelogModalOpen] = useState(false); + const [changelogEntries, setChangelogEntries] = useState>({}); const [qrModalOpen, setQrModalOpen] = useState(false); const [generateMockModalOpen, setGenerateMockModalOpen] = useState(false); const [clearMockModalOpen, setClearMockModalOpen] = useState(false); @@ -71,6 +68,19 @@ export default function Header({ choices, dayIndex }: Props) { } }, [auth?.login]); + useEffect(() => { + if (!auth?.login) return; + const lastSeen = localStorage.getItem(LAST_SEEN_CHANGELOG_KEY) ?? undefined; + getChangelogs({ query: lastSeen ? { since: lastSeen } : {} }).then(response => { + const entries = response.data; + if (!entries || Object.keys(entries).length === 0) return; + setChangelogEntries(entries); + setChangelogModalOpen(true); + const newestDate = Object.keys(entries).sort((a, b) => b.localeCompare(a))[0]; + localStorage.setItem(LAST_SEEN_CHANGELOG_KEY, newestDate); + }); + }, [auth?.login]); + const closeSettingsModal = () => { setSettingsModalOpen(false); } @@ -198,7 +208,17 @@ export default function Header({ choices, dayIndex }: Props) { Generování QR kódů navigate(STATS_URL)}>Statistiky navigate(VECERE_URL)}>Večeře - setChangelogModalOpen(true)}>Novinky + { + getChangelogs().then(response => { + const entries = response.data ?? {}; + setChangelogEntries(entries); + setChangelogModalOpen(true); + const dates = Object.keys(entries).sort((a, b) => b.localeCompare(a)); + if (dates.length > 0) { + localStorage.setItem(LAST_SEEN_CHANGELOG_KEY, dates[0]); + } + }); + }}>Novinky {IS_DEV && ( <> @@ -238,16 +258,24 @@ export default function Header({ choices, dayIndex }: Props) { /> )} - setChangelogModalOpen(false)}> + setChangelogModalOpen(false)} size="lg">

Novinky

-
    - {CHANGELOG.map((item, index) => ( -
  • {item}
  • - ))} -
+ {Object.keys(changelogEntries).sort((a, b) => b.localeCompare(a)).map(date => ( +
+ {formatDateString(date)} +
    + {changelogEntries[date].map((item, index) => ( +
  • {item}
  • + ))} +
+
+ ))} + {Object.keys(changelogEntries).length === 0 && ( +

Žádné novinky.

+ )}
+ + + + + + + + + + + + {diners.map(d => { + const isPayer = d.login === payerLogin; + const foodNames = d.selectedFoods.map(i => menu?.food?.[i]?.name).filter(Boolean).join(', '); + const total = getTotal(d); + return ( + + + + + + + + + ); + })} + +
StrávníkJídlaPříplatekDýškoCelkem
+ {isPayer ? ( + plátce + ) : ( + handleInclude(d.login, e.target.checked)} + /> + )} + {d.login} + + {foodNames || } + {hasMenu && d.baseAmount > 0 && ({d.baseAmount} Kč)} + {d.baseAmountParseFailed && } + + + {!isPayer && ( +
+ handleSurchargeText(d.login, e.target.value)} + disabled={!d.included} + size="sm" + onKeyDown={e => e.stopPropagation()} + /> + handleSurchargeAmount(d.login, e.target.value)} + disabled={!d.included} + size="sm" + style={{ width: 70 }} + onKeyDown={e => e.stopPropagation()} + /> +
+ )} +
+ {!isPayer && d.included ? `${tipPerPerson} Kč` : '—'} + + {!isPayer ? `${total} Kč` : '—'} +
+ +
+ + setTipTotal(sanitizeAmount(e.target.value))} + size="sm" + style={{ width: 100 }} + onKeyDown={e => e.stopPropagation()} + /> + + {includedDiners.length > 0 && tipPerPerson > 0 + ? `(${tipPerPerson} Kč / osoba)` + : ''} + +
+ + )} + + + {!success && ( + <> + + Příjemci: {includedDiners.length} + + + + + )} + {success && ( + + )} + + + ); +} diff --git a/client/src/utils/parsePrice.ts b/client/src/utils/parsePrice.ts new file mode 100644 index 0000000..4863b37 --- /dev/null +++ b/client/src/utils/parsePrice.ts @@ -0,0 +1,11 @@ +/** + * Parsuje cenu ve formátu "135 Kč", "135,50 Kč" nebo "135.50 Kč" na číslo. + * Vrátí null při selhání. + */ +export function parsePriceCzk(raw: string | undefined): number | null { + if (!raw) return null; + const m = raw.replace(',', '.').match(/(\d+(?:\.\d+)?)/); + if (!m) return null; + const n = parseFloat(m[1]); + return Number.isFinite(n) ? n : null; +} diff --git a/e2e/.gitignore b/e2e/.gitignore new file mode 100644 index 0000000..945fcd0 --- /dev/null +++ b/e2e/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +playwright-report/ +test-results/ diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 0000000..5f1ef37 --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,16 @@ +{ + "name": "@luncher/e2e", + "version": "1.0.0", + "private": true, + "scripts": { + "test": "playwright test", + "test:ui": "playwright test --ui", + "test:debug": "playwright test --debug", + "report": "playwright show-report" + }, + "devDependencies": { + "@playwright/test": "^1.50.0", + "@types/node": "^22.0.0", + "typescript": "^5.9.3" + } +} diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 0000000..9301772 --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,60 @@ +import { defineConfig, devices } from '@playwright/test'; +import path from 'path'; + +// Use 127.0.0.1 explicitly — on Node.js 18+/Windows, `localhost` may resolve to ::1 +// (IPv6) while the HTTP server only binds to 0.0.0.0 (IPv4), causing the webServer +// readiness poll to time out even though the server is listening. +const BASE_URL = process.env.E2E_BASE_URL ?? 'http://127.0.0.1:3001'; + +// Server env vars injected for local runs. In CI these are set at the step level. +const serverEnv: Record = { + NODE_ENV: 'test', + MOCK_DATA: 'true', + STORAGE: process.env.STORAGE ?? 'json', + JWT_SECRET: process.env.JWT_SECRET ?? 'e2e-test-secret-min-32-chars-aaaa', + HTTP_REMOTE_USER_ENABLED: 'true', + HTTP_REMOTE_USER_HEADER_NAME: 'remote-user', + HTTP_REMOTE_TRUSTED_IPS: process.env.HTTP_REMOTE_TRUSTED_IPS ?? '127.0.0.1,::1,::ffff:127.0.0.1', +}; +if (process.env.REDIS_HOST) { + serverEnv.REDIS_HOST = process.env.REDIS_HOST; + serverEnv.REDIS_PORT = process.env.REDIS_PORT ?? '6379'; +} + +export default defineConfig({ + testDir: './tests', + timeout: 30_000, + retries: process.env.CI ? 1 : 0, + workers: 1, + reporter: [['list'], ['html', { open: 'never' }]], + use: { + baseURL: BASE_URL, + // Default: every test authenticates as e2e-user via trusted header. + // Tests that need the real login form should override this in their own context. + extraHTTPHeaders: { + 'remote-user': 'e2e-user', + }, + trace: 'retain-on-failure', + video: 'retain-on-failure', + }, + projects: [ + { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, + { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, + ], + // Pre-built server must be started before tests. In CI the step does this + // explicitly. Locally: build types+server+client, cp -r client/dist server/public, + // then `cd e2e && yarn test` OR let webServer below do it if reuseExistingServer=true + // is set and the server is already running. + webServer: { + command: 'node dist/server/src/index.js', + cwd: path.resolve(__dirname, '../server'), + // Poll a dedicated health endpoint — polling '/' can stall in Express 5 when + // server/public/ doesn't exist in the working directory (no finalhandler match). + url: `http://127.0.0.1:3001/api/health`, + timeout: 15_000, + reuseExistingServer: !process.env.CI, + env: serverEnv, + stdout: 'pipe', + stderr: 'pipe', + }, +}); diff --git a/e2e/tests/helpers.ts b/e2e/tests/helpers.ts new file mode 100644 index 0000000..5f05e21 --- /dev/null +++ b/e2e/tests/helpers.ts @@ -0,0 +1,24 @@ +import { Page, APIRequestContext } from '@playwright/test'; + +/** Přihlásí uživatele přes POST /api/login a uloží JWT do localStorage. */ +export async function loginViaApi(page: Page, login: string): Promise { + const response = await page.request.post('/api/login', { + headers: { 'Content-Type': 'application/json', 'remote-user': login }, + data: {}, + }); + const token = await response.json() as string; + await page.goto('/'); + await page.evaluate((t) => localStorage.setItem('token', t), token); +} + +/** Vyčistí stav dne pro zadaný dayIndex (0=pondělí…4=pátek) přes dev API. + * /api/dev/* vyžaduje JWT – nejdřív získáme token přes /api/login. + */ +export async function clearDay(request: APIRequestContext, dayIndex = 4): Promise { + const loginResp = await request.post('/api/login', { data: {} }); + const token = await loginResp.json() as string; + await request.post('/api/dev/clear', { + headers: { Authorization: `Bearer ${token}` }, + data: { dayIndex }, + }); +} diff --git a/e2e/tests/login.spec.ts b/e2e/tests/login.spec.ts new file mode 100644 index 0000000..0df71d0 --- /dev/null +++ b/e2e/tests/login.spec.ts @@ -0,0 +1,50 @@ +import { test, expect } from '@playwright/test'; + +// Tento test záměrně NEPOUŽÍVÁ trusted-header – testuje reálný login formulář. +test.use({ extraHTTPHeaders: {} }); + +test('uživatel se přihlásí formulářem a uvidí obsah aplikace', async ({ page }) => { + // Server běží s HTTP_REMOTE_USER_ENABLED=true, takže POST /api/login vždy vyžaduje + // hlavičku remote-user. Zachytíme požadavky z formuláře (mají tělo s polem login) + // a přidáme hlavičku; požadavek auto-loginu (bez těla) projde bez hlavičky a selže, + // čímž formulář zůstane viditelný. + await page.route('**/api/login', async (route) => { + const body = route.request().postData(); + let login: string | undefined; + try { login = body ? JSON.parse(body)?.login : undefined; } catch {} + await route.continue({ + headers: login + ? { ...route.request().headers(), 'remote-user': login } + : route.request().headers(), + }); + }); + + await page.goto('/'); + + // Formulář musí být viditelný – auto-login selhal (nepřišla hlavička) + const loginInput = page.locator('#login-input'); + await expect(loginInput).toBeVisible({ timeout: 10_000 }); + + // Vyplnění loginu a odeslání Enterem + await loginInput.fill('testuser'); + await loginInput.press('Enter'); + + // Po přihlášení musí zmizet login formulář + await expect(loginInput).not.toBeVisible({ timeout: 10_000 }); + + // JWT musí být uloženo v localStorage jako 3-dílný token + const token = await page.evaluate(() => localStorage.getItem('token')); + expect(token).toBeTruthy(); + expect((token as string).split('.')).toHaveLength(3); +}); + +test('trusted-header přihlášení proběhne automaticky bez formuláře', async ({ page, context }) => { + // Obnoví trusted header (přepíše prázdný extraHTTPHeaders z test.use výše) + await context.setExtraHTTPHeaders({ 'remote-user': 'e2e-auto-user' }); + await page.goto('/'); + + // Login formulář by se neměl nikdy zobrazit, nebo se ihned schová + await page.waitForLoadState('networkidle'); + const loginInput = page.locator('#login-input'); + await expect(loginInput).not.toBeVisible({ timeout: 5_000 }); +}); diff --git a/e2e/tests/pick-food.spec.ts b/e2e/tests/pick-food.spec.ts new file mode 100644 index 0000000..64cbd91 --- /dev/null +++ b/e2e/tests/pick-food.spec.ts @@ -0,0 +1,68 @@ +import { test, expect } from '@playwright/test'; +import { clearDay } from './helpers'; + +test.beforeEach(async ({ page, request }) => { + // Vyčistíme volby dne, aby testy neovlivnily navzájem + await clearDay(request); + await page.goto('/'); + await page.waitForLoadState('networkidle'); + // Počkáme, až se zobrazí volba stravování + await expect(page.locator('.choice-section select').first()).toBeVisible({ timeout: 10_000 }); +}); + +test('výběr restaurace zobrazí seznam jídel', async ({ page }) => { + const locationSelect = page.locator('.choice-section select').first(); + + // Vybereme Sladovnickou – mock menu existuje + await locationSelect.selectOption('SLADOVNICKA'); + + // Po výběru restaurace se zobrazí druhý select s jídly + const foodSelect = page.locator('.choice-section select').nth(1); + await expect(foodSelect).toBeVisible({ timeout: 5_000 }); + + // Select musí obsahovat alespoň 2 možnosti (empty + ≥1 jídlo) + const options = foodSelect.locator('option'); + expect(await options.count()).toBeGreaterThan(1); +}); + +test('výběr jídla se uloží a přetrvá po reload', async ({ page }) => { + const locationSelect = page.locator('.choice-section select').first(); + await locationSelect.selectOption('SLADOVNICKA'); + + const foodSelect = page.locator('.choice-section select').nth(1); + await expect(foodSelect).toBeVisible({ timeout: 5_000 }); + + // Vybereme první nenulovou možnost + const options = await foodSelect.locator('option:not([value=""])').all(); + if (options.length === 0) { + test.skip(); // Mock data nejsou dostupná pro tuto restauraci + } + const firstValue = await options[0].getAttribute('value'); + await foodSelect.selectOption({ value: firstValue! }); + + // Počkáme, až se volba přenese na server + await page.waitForLoadState('networkidle'); + + // Po reload musí volba přetrvat v tabulce choices + await page.reload(); + await page.waitForLoadState('networkidle'); + const choicesTable = page.locator('.choices-table'); + await expect(choicesTable).toBeVisible({ timeout: 5_000 }); + await expect(choicesTable.locator('text=Sladovnická')).toBeVisible(); +}); + +test('přepnutí na NEOBEDVAM odstraní výběr restaurace', async ({ page }) => { + // Nejprve zvolíme restauraci + const locationSelect = page.locator('.choice-section select').first(); + await locationSelect.selectOption('SLADOVNICKA'); + await page.waitForLoadState('networkidle'); + + // Přepneme na "Neobědvám" + await locationSelect.selectOption('NEOBEDVAM'); + await page.waitForLoadState('networkidle'); + + // Tabulka choices musí zobrazovat "Neobědvám" + const choicesTable = page.locator('.choices-table'); + await expect(choicesTable).toBeVisible({ timeout: 5_000 }); + await expect(choicesTable.locator('text=Neobědvám')).toBeVisible(); +}); diff --git a/e2e/tests/pizza-day.spec.ts b/e2e/tests/pizza-day.spec.ts new file mode 100644 index 0000000..e3e4a23 --- /dev/null +++ b/e2e/tests/pizza-day.spec.ts @@ -0,0 +1,83 @@ +import { test, expect } from '@playwright/test'; +import { clearDay } from './helpers'; + +// Pizza day testy musí běžet sekvenčně (sdílejí stav mock dne) +test.describe.serial('pizza day životní cyklus', () => { + + test.beforeEach(async ({ request }) => { + // Vyčistíme data mock dne před každým testem + await clearDay(request); + }); + + test('zobrazí sekci Pizza Day bez aktivního dne', async ({ page }) => { + await page.goto('/'); + await page.waitForLoadState('networkidle'); + // Sekce pizza-section se zobrazí jen pokud má uživatel zvolenou možnost "Pizza day" + await page.locator('select').selectOption({ label: 'Pizza day' }); + await page.waitForLoadState('networkidle'); + const pizzaSection = page.locator('.pizza-section'); + await expect(pizzaSection).toBeVisible({ timeout: 10_000 }); + await expect(pizzaSection.locator('text=není aktuálně založen')).toBeVisible(); + }); + + test('vytvoří, uzamkne a dokončí pizza day', async ({ page }) => { + // Tento test má více kroků a server při MOCK_DATA=true záměrně zpožďuje scraping pizz o 3s + test.setTimeout(60_000); + await page.goto('/'); + await page.waitForLoadState('networkidle'); + // Sekce pizza-section se zobrazí jen pokud má uživatel zvolenou možnost "Pizza day" + await page.locator('select').selectOption({ label: 'Pizza day' }); + await page.waitForLoadState('networkidle'); + + // Přijmeme všechny window.confirm() dialogy v celém testu (vytvoření i doručení pizza dne) + page.on('dialog', dialog => dialog.accept()); + + // --- CREATED --- + const createBtn = page.locator('.pizza-section button', { hasText: 'Založit Pizza day' }); + await expect(createBtn).toBeVisible({ timeout: 10_000 }); + // Čekáme na odpověď API před reloadem – jinak by reload přerušil probíhající request + // Server s MOCK_DATA=true záměrně zpožďuje stahování pizz o 3s, proto velkorysý timeout + const createResponse = page.waitForResponse( + resp => resp.url().includes('/api/pizzaDay/create'), + { timeout: 15_000 }, + ); + await createBtn.click(); + await createResponse; + await page.reload(); + await page.waitForLoadState('networkidle'); + await expect(page.locator('.pizza-section')).toContainText('spravován uživatelem', { timeout: 5_000 }); + + // Přidáme pizzu přes API (obejde komplex SelectSearch) + const token = await page.evaluate(() => localStorage.getItem('token')); + const addResp = await page.request.post('/api/pizzaDay/add', { + headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + data: { pizzaIndex: 0, pizzaSizeIndex: 0 }, + }); + expect(addResp.ok()).toBeTruthy(); + + // Reload – server aktualizoval data přes WebSocket, ale reload je jistější + await page.reload(); + await page.waitForLoadState('networkidle'); + + // --- LOCK --- + const lockBtn = page.locator('.pizza-section button', { hasText: 'Uzamknout' }); + await expect(lockBtn).toBeEnabled({ timeout: 5_000 }); + await lockBtn.click(); + await page.waitForLoadState('networkidle'); + await expect(page.locator('.pizza-section')).toContainText('uzamčeny', { timeout: 5_000 }); + + // --- ORDERED --- + const orderBtn = page.locator('.pizza-section button', { hasText: 'Objednáno' }); + await expect(orderBtn).toBeEnabled({ timeout: 5_000 }); + await orderBtn.click(); + await page.waitForLoadState('networkidle'); + await expect(page.locator('.pizza-section')).toContainText('objednány', { timeout: 5_000 }); + + // --- DELIVERED --- + const deliverBtn = page.locator('.pizza-section button', { hasText: 'Doručeno' }); + await expect(deliverBtn).toBeVisible({ timeout: 5_000 }); + await deliverBtn.click(); + await page.waitForLoadState('networkidle'); + await expect(page.locator('.pizza-section')).toContainText('doručeny', { timeout: 5_000 }); + }); +}); diff --git a/e2e/tests/qr.spec.ts b/e2e/tests/qr.spec.ts new file mode 100644 index 0000000..20777c8 --- /dev/null +++ b/e2e/tests/qr.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from '@playwright/test'; + +test.beforeEach(async ({ page, request }) => { + // Naseedujeme 5 uživatelů pro dnešní den – GenerateQrModal pracuje se stávajícími choices + await request.post('/api/dev/generate', { data: { dayIndex: 4, count: 5 } }); + + // Přednastavíme bankovní účet v localStorage (SettingsContext čte z LS při inicializaci) + await page.goto('/'); + await page.evaluate(() => { + localStorage.setItem('bank_account_number', '2400000000/2010'); + localStorage.setItem('bank_account_holder_name', 'Test User'); + }); + // Reload tak, aby SettingsContext načetl nové hodnoty z localStorage + await page.reload(); + await page.waitForLoadState('networkidle'); +}); + +test('Nastavení ukládají číslo účtu a jméno do localStorage', async ({ page }) => { + // Otevření nastavení + await page.locator('#basic-nav-dropdown').click(); + await page.locator('text=Nastavení').click(); + + // Modal musí být viditelný + await expect(page.locator('.modal-title')).toContainText('Nastavení', { timeout: 5_000 }); + + // Změníme číslo účtu – pressSequentially zajistí spuštění React onChange na každý znak + // Číslo 1000000005 je platné (kontrolní součet mod 11 = 0), jinak by validace zamítla uložení + const accountInput = page.getByPlaceholder('123456-1234567890/1234'); + await accountInput.click({ clickCount: 3 }); + await accountInput.pressSequentially('1000000005/5500'); + + // Změníme jméno + const nameInput = page.getByPlaceholder('Jan Novák'); + await nameInput.click({ clickCount: 3 }); + await nameInput.pressSequentially('Nové Jméno'); + + // Uložíme a počkáme na zavření modalu + await page.locator('.modal-footer button', { hasText: 'Uložit' }).click(); + await expect(page.locator('.modal')).not.toBeVisible({ timeout: 5_000 }); + + // Ověříme v localStorage + const bankAccount = await page.evaluate(() => localStorage.getItem('bank_account_number')); + const holderName = await page.evaluate(() => localStorage.getItem('bank_account_holder_name')); + expect(bankAccount).toBe('1000000005/5500'); + expect(holderName).toBe('Nové Jméno'); +}); + +test('otevře modal Generování QR kódů pokud je nastaven účet', async ({ page }) => { + // Otevření dropdown menu + await page.locator('#basic-nav-dropdown').click(); + await page.locator('text=Generování QR kódů').click(); + + // Modal se otevře + await expect(page.locator('.modal')).toBeVisible({ timeout: 5_000 }); + // Modal musí obsahovat seznam uživatelů nebo prázdný stav + await expect(page.locator('.modal-body')).toBeVisible(); +}); + +test('upozorní pokud není nastaven bankovní účet', async ({ page }) => { + // Odebereme nastavení účtu + await page.evaluate(() => { + localStorage.removeItem('bank_account_number'); + localStorage.removeItem('bank_account_holder_name'); + }); + await page.reload(); + await page.waitForLoadState('networkidle'); + + // Dialog místo modalu + page.on('dialog', async dialog => { + expect(dialog.message()).toContain('číslo účtu'); + await dialog.accept(); + }); + + await page.locator('#basic-nav-dropdown').click(); + await page.locator('text=Generování QR kódů').click(); + + // Modal se NESMÍ otevřít + await expect(page.locator('.modal')).not.toBeVisible({ timeout: 3_000 }); +}); diff --git a/e2e/tests/view-menus.spec.ts b/e2e/tests/view-menus.spec.ts new file mode 100644 index 0000000..a8f6b8e --- /dev/null +++ b/e2e/tests/view-menus.spec.ts @@ -0,0 +1,39 @@ +import { test, expect } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + // Trusted-header login runs automatically when Login mounts. + // networkidle zaručí, že fetch('/api/data') byl dokončen. + await page.goto('/'); + await page.waitForLoadState('networkidle'); +}); + +test('zobrazí mock datum 10.01.2025', async ({ page }) => { + // MOCK_DATA=true pins today to 2025-01-10 + await expect(page.locator('text=10.01')).toBeVisible({ timeout: 10_000 }); +}); + +test('zobrazí čtyři restaurační karty z mock dat', async ({ page }) => { + // Každá restaurace je obalena v .restaurant-card + const cards = page.locator('.restaurant-card'); + await expect(cards).toHaveCount(4, { timeout: 10_000 }); +}); + +test('zobrazí alespoň jedno jídlo v menu každé restaurace', async ({ page }) => { + await expect(page.locator('.restaurant-card').first()).toBeVisible({ timeout: 10_000 }); + + // Každá karta musí mít aspoň jeden řádek v .food-table + const cards = page.locator('.restaurant-card'); + const count = await cards.count(); + for (let i = 0; i < count; i++) { + const card = cards.nth(i); + const rows = card.locator('.food-table tr'); + expect(await rows.count()).toBeGreaterThan(0); + } +}); + +test('zobrazí volbu stravování před menu', async ({ page }) => { + // Sekce .choice-section obsahuje select pro výběr stravování + const choiceSection = page.locator('.choice-section'); + await expect(choiceSection).toBeVisible({ timeout: 10_000 }); + await expect(choiceSection.locator('select').first()).toBeVisible(); +}); diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json new file mode 100644 index 0000000..9b8b996 --- /dev/null +++ b/e2e/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["**/*.ts"] +} diff --git a/e2e/yarn.lock b/e2e/yarn.lock new file mode 100644 index 0000000..91d4403 --- /dev/null +++ b/e2e/yarn.lock @@ -0,0 +1,46 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@playwright/test@^1.50.0": + version "1.59.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.59.1.tgz#5c4d38eac84a61527af466602ae20277685a02d6" + integrity sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg== + dependencies: + playwright "1.59.1" + +"@types/node@^22.0.0": + version "22.19.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.19.17.tgz#09c71fb34ba2510f8ac865361b1fcb9552b8a581" + integrity sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q== + dependencies: + undici-types "~6.21.0" + +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +playwright-core@1.59.1: + version "1.59.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.59.1.tgz#d8a2b28bcb8f2bd08ef3df93b02ae83c813244b2" + integrity sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg== + +playwright@1.59.1: + version "1.59.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.59.1.tgz#f7b0ca61637ae25264cec370df671bbe1f368a4a" + integrity sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw== + dependencies: + playwright-core "1.59.1" + optionalDependencies: + fsevents "2.3.2" + +typescript@^5.9.3: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 6d03db7..0000000 --- a/package-lock.json +++ /dev/null @@ -1,3941 +0,0 @@ -{ - "name": "Luncher", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "gitnexus": "^1.4.1" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", - "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@hono/node-server": { - "version": "1.19.11", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", - "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@huggingface/jinja": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.6.tgz", - "integrity": "sha512-MyMWyLnjqo+KRJYSH7oWNbsOn5onuIvfXYPcc0WOGxU0eHUV7oAYUoQTl2BMdu7ml+ea/bu11UM+EshbeHwtIA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@huggingface/transformers": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.8.1.tgz", - "integrity": "sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA==", - "license": "Apache-2.0", - "dependencies": { - "@huggingface/jinja": "^0.5.3", - "onnxruntime-node": "1.21.0", - "onnxruntime-web": "1.22.0-dev.20250409-89f8206ba4", - "sharp": "^0.34.1" - } - }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@isaacs/cliui": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", - "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", - "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", - "license": "MIT", - "dependencies": { - "@hono/node-server": "^1.19.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "license": "BSD-3-Clause" - }, - "node_modules/@types/node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", - "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/aproba": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", - "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", - "license": "ISC" - }, - "node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/cli-progress": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", - "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", - "license": "MIT", - "dependencies": { - "string-width": "^4.2.3" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cmake-js": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/cmake-js/-/cmake-js-7.4.0.tgz", - "integrity": "sha512-Lw0JxEHrmk+qNj1n9W9d4IvkDdYTBn7l2BW6XmtLj7WPpIo2shvxUy+YokfjMxAAOELNonQwX3stkPhM5xSC2Q==", - "license": "MIT", - "dependencies": { - "axios": "^1.6.5", - "debug": "^4", - "fs-extra": "^11.2.0", - "memory-stream": "^1.0.0", - "node-api-headers": "^1.1.0", - "npmlog": "^6.0.2", - "rc": "^1.2.7", - "semver": "^7.5.4", - "tar": "^6.2.0", - "url-join": "^4.0.1", - "which": "^2.0.2", - "yargs": "^17.7.2" - }, - "bin": { - "cmake-js": "bin/cmake-js" - }, - "engines": { - "node": ">= 14.15.0" - } - }, - "node_modules/cmake-js/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/cmake-js/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "license": "ISC", - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "license": "ISC" - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "license": "MIT" - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "license": "MIT" - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "license": "MIT" - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.14.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", - "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", - "license": "MIT", - "dependencies": { - "ip-address": "10.1.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/flatbuffers": { - "version": "25.9.23", - "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", - "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", - "license": "Apache-2.0" - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/gauge/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gitnexus": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/gitnexus/-/gitnexus-1.4.1.tgz", - "integrity": "sha512-cW5z963P1tU6zNI5r3wG7+L4U/Qf46Kqdcq7c5asx+yRD7Oaft3rLNHda4IZUoDQN3DGsz6ZS67gM8Uy1wayFA==", - "hasInstallScript": true, - "license": "PolyForm-Noncommercial-1.0.0", - "dependencies": { - "@huggingface/transformers": "^3.0.0", - "@modelcontextprotocol/sdk": "^1.0.0", - "cli-progress": "^3.12.0", - "commander": "^12.0.0", - "cors": "^2.8.5", - "express": "^4.19.2", - "glob": "^11.0.0", - "graphology": "^0.25.4", - "graphology-indices": "^0.17.0", - "graphology-utils": "^2.3.0", - "kuzu": "^0.11.3", - "lru-cache": "^11.0.0", - "mnemonist": "^0.39.0", - "pandemonium": "^2.4.0", - "tree-sitter": "^0.21.0", - "tree-sitter-c": "^0.21.0", - "tree-sitter-c-sharp": "^0.21.0", - "tree-sitter-cpp": "^0.22.0", - "tree-sitter-go": "^0.21.0", - "tree-sitter-java": "^0.21.0", - "tree-sitter-javascript": "^0.21.0", - "tree-sitter-kotlin": "^0.3.8", - "tree-sitter-php": "^0.23.12", - "tree-sitter-python": "^0.21.0", - "tree-sitter-rust": "^0.21.0", - "tree-sitter-typescript": "^0.21.0", - "uuid": "^13.0.0" - }, - "bin": { - "gitnexus": "dist/cli/index.js" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "tree-sitter-swift": "^0.6.0" - } - }, - "node_modules/glob": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", - "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "BlueOak-1.0.0", - "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "license": "BSD-3-Clause", - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/graphology": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/graphology/-/graphology-0.25.4.tgz", - "integrity": "sha512-33g0Ol9nkWdD6ulw687viS8YJQBxqG5LWII6FI6nul0pq6iM2t5EKquOTFDbyTblRB3O9I+7KX4xI8u5ffekAQ==", - "license": "MIT", - "dependencies": { - "events": "^3.3.0", - "obliterator": "^2.0.2" - }, - "peerDependencies": { - "graphology-types": ">=0.24.0" - } - }, - "node_modules/graphology-indices": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/graphology-indices/-/graphology-indices-0.17.0.tgz", - "integrity": "sha512-A7RXuKQvdqSWOpn7ZVQo4S33O0vCfPBnUSf7FwE0zNCasqwZVUaCXePuWo5HBpWw68KJcwObZDHpFk6HKH6MYQ==", - "license": "MIT", - "dependencies": { - "graphology-utils": "^2.4.2", - "mnemonist": "^0.39.0" - }, - "peerDependencies": { - "graphology-types": ">=0.20.0" - } - }, - "node_modules/graphology-types": { - "version": "0.24.8", - "resolved": "https://registry.npmjs.org/graphology-types/-/graphology-types-0.24.8.tgz", - "integrity": "sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==", - "license": "MIT", - "peer": true - }, - "node_modules/graphology-utils": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/graphology-utils/-/graphology-utils-2.5.2.tgz", - "integrity": "sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ==", - "license": "MIT", - "peerDependencies": { - "graphology-types": ">=0.23.0" - } - }, - "node_modules/guid-typescript": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", - "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", - "license": "ISC" - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "license": "ISC" - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hono": { - "version": "4.12.8", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.8.tgz", - "integrity": "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==", - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", - "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^9.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jose": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.1.tgz", - "integrity": "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "license": "ISC" - }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/kuzu": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/kuzu/-/kuzu-0.11.3.tgz", - "integrity": "sha512-4+hD3Y+YMV3e0uiqTv1/GUal47D04l8qluw1WFWg8Nx3k7rLsHG1Pmq9WHIOlf1742svxQvTYQiuY6oS1qxAZA==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "cmake-js": "^7.3.0", - "node-addon-api": "^6.0.0" - } - }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, - "node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memory-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/memory-stream/-/memory-stream-1.0.0.tgz", - "integrity": "sha512-Wm13VcsPIMdG96dzILfij09PvuS3APtcKNh7M28FsCA/w6+1mjR7hhPmfFNoilX9xU7wTdhsH5lJAm6XNzdtww==", - "license": "MIT", - "dependencies": { - "readable-stream": "^3.4.0" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mnemonist": { - "version": "0.39.8", - "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz", - "integrity": "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==", - "license": "MIT", - "dependencies": { - "obliterator": "^2.0.1" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-addon-api": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", - "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", - "license": "MIT" - }, - "node_modules/node-api-headers": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/node-api-headers/-/node-api-headers-1.8.0.tgz", - "integrity": "sha512-jfnmiKWjRAGbdD1yQS28bknFM1tbHC1oucyuMPjmkEs+kpiu76aRs40WlTmBmyEgzDM76ge1DQ7XJ3R5deiVjQ==", - "license": "MIT" - }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/obliterator": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", - "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", - "license": "MIT" - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onnxruntime-common": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz", - "integrity": "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==", - "license": "MIT" - }, - "node_modules/onnxruntime-node": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.21.0.tgz", - "integrity": "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==", - "hasInstallScript": true, - "license": "MIT", - "os": [ - "win32", - "darwin", - "linux" - ], - "dependencies": { - "global-agent": "^3.0.0", - "onnxruntime-common": "1.21.0", - "tar": "^7.0.1" - } - }, - "node_modules/onnxruntime-node/node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/onnxruntime-node/node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/onnxruntime-node/node_modules/tar": { - "version": "7.5.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", - "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/onnxruntime-node/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/onnxruntime-web": { - "version": "1.22.0-dev.20250409-89f8206ba4", - "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.22.0-dev.20250409-89f8206ba4.tgz", - "integrity": "sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==", - "license": "MIT", - "dependencies": { - "flatbuffers": "^25.1.24", - "guid-typescript": "^1.0.9", - "long": "^5.2.3", - "onnxruntime-common": "1.22.0-dev.20250409-89f8206ba4", - "platform": "^1.3.6", - "protobufjs": "^7.2.4" - } - }, - "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { - "version": "1.22.0-dev.20250409-89f8206ba4", - "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.22.0-dev.20250409-89f8206ba4.tgz", - "integrity": "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ==", - "license": "MIT" - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, - "node_modules/pandemonium": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/pandemonium/-/pandemonium-2.4.1.tgz", - "integrity": "sha512-wRqjisUyiUfXowgm7MFH2rwJzKIr20rca5FsHXCMNm1W5YPP1hCtrZfgmQ62kP7OZ7Xt+cR858aB28lu5NX55g==", - "license": "MIT", - "dependencies": { - "mnemonist": "^0.39.2" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/platform": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", - "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", - "license": "MIT" - }, - "node_modules/protobufjs": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", - "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "license": "BSD-3-Clause", - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/router/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "license": "MIT" - }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "license": "MIT", - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "license": "BSD-3-Clause" - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tree-sitter": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz", - "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^8.0.0", - "node-gyp-build": "^4.8.0" - } - }, - "node_modules/tree-sitter-c": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/tree-sitter-c/-/tree-sitter-c-0.21.4.tgz", - "integrity": "sha512-IahxFIhXiY15SUlrt2upBiKSBGdOaE1fjKLK1Ik5zxqGHf6T1rvr3IJrovbsE5sXhypx7Hnmf50gshsppaIihA==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^8.0.0", - "node-gyp-build": "^4.8.1" - }, - "peerDependencies": { - "tree-sitter": "^0.21.0" - }, - "peerDependenciesMeta": { - "tree_sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-c-sharp": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/tree-sitter-c-sharp/-/tree-sitter-c-sharp-0.21.3.tgz", - "integrity": "sha512-TVsl5EhmqetO/mhzDPVnMK6TPFnpNMKP0OTNuAQIprshk5Hx672ODRxoIoG5WqvUUlsnBu8J0zmn35hmJqelsA==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^8.0.0", - "node-gyp-build": "^4.8.1" - }, - "peerDependencies": { - "tree-sitter": "^0.21.0" - }, - "peerDependenciesMeta": { - "tree_sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-c-sharp/node_modules/node-addon-api": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", - "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tree-sitter-c/node_modules/node-addon-api": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", - "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tree-sitter-cli": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.23.2.tgz", - "integrity": "sha512-kPPXprOqREX+C/FgUp2Qpt9jd0vSwn+hOgjzVv/7hapdoWpa+VeWId53rf4oNNd29ikheF12BYtGD/W90feMbA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "bin": { - "tree-sitter": "cli.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/tree-sitter-cpp": { - "version": "0.22.3", - "resolved": "https://registry.npmjs.org/tree-sitter-cpp/-/tree-sitter-cpp-0.22.3.tgz", - "integrity": "sha512-p7w5903L/koqTQFVDwyyX0vjioxoZu2G4zT2ZHVG8DvLQbWN6OjNAqfMsCi+WdVkfMgU+7j06hS8i3j6Q0sPNQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^8.0.0", - "node-gyp-build": "^4.8.1" - }, - "peerDependencies": { - "tree-sitter": "^0.21.1" - }, - "peerDependenciesMeta": { - "tree_sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-cpp/node_modules/node-addon-api": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", - "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tree-sitter-go": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/tree-sitter-go/-/tree-sitter-go-0.21.2.tgz", - "integrity": "sha512-aMFwjsB948nWhURiIxExK8QX29JYKs96P/IfXVvluVMRJZpL04SREHsdOZHYqJr1whkb7zr3/gWHqqvlkczmvw==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^8.1.0", - "node-gyp-build": "^4.8.1" - }, - "peerDependencies": { - "tree-sitter": "^0.21.0" - }, - "peerDependenciesMeta": { - "tree_sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-go/node_modules/node-addon-api": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", - "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tree-sitter-java": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/tree-sitter-java/-/tree-sitter-java-0.21.0.tgz", - "integrity": "sha512-CKJiTo1uc3SUsgEcaZgufGx8my6dzihy8JR/JsJH40Tj3uSe2/eFLk+0q+fpbosGAyY4YiXJtEoFB2O4bS2yOw==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^8.0.0", - "node-gyp-build": "^4.8.0" - }, - "peerDependencies": { - "tree-sitter": "^0.21.0" - }, - "peerDependenciesMeta": { - "tree_sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-java/node_modules/node-addon-api": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", - "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tree-sitter-javascript": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/tree-sitter-javascript/-/tree-sitter-javascript-0.21.4.tgz", - "integrity": "sha512-Lrk8yahebwrwc1sWJE9xPcz1OnnqiEV7Dh5fbN6EN3wNAdu9r06HpTqLqDwUUbnG4EB46Sfk+FJFAOldfoKLOw==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^8.0.0", - "node-gyp-build": "^4.8.1" - }, - "peerDependencies": { - "tree-sitter": "^0.21.1" - }, - "peerDependenciesMeta": { - "tree_sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-javascript/node_modules/node-addon-api": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", - "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tree-sitter-kotlin": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/tree-sitter-kotlin/-/tree-sitter-kotlin-0.3.8.tgz", - "integrity": "sha512-A4obq6bjzmYrA+F0JLLoheFPcofFkctNaZSpnDd+GPn1SfVZLY4/GG4C0cYVBTOShuPBGGAOPLM1JWLZQV4m1g==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^7.1.0", - "node-gyp-build": "^4.8.0" - }, - "peerDependencies": { - "tree-sitter": "^0.21.0" - }, - "peerDependenciesMeta": { - "tree_sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-kotlin/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT" - }, - "node_modules/tree-sitter-php": { - "version": "0.23.12", - "resolved": "https://registry.npmjs.org/tree-sitter-php/-/tree-sitter-php-0.23.12.tgz", - "integrity": "sha512-VwkBVOahhC2NYXK/Fuqq30NxuL/6c2hmbxEF4jrB7AyR5rLc7nT27mzF3qoi+pqx9Gy2AbXnGezF7h4MeM6YRA==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^8.2.2", - "node-gyp-build": "^4.8.2" - }, - "peerDependencies": { - "tree-sitter": "^0.21.1" - }, - "peerDependenciesMeta": { - "tree-sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-php/node_modules/node-addon-api": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", - "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tree-sitter-python": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/tree-sitter-python/-/tree-sitter-python-0.21.0.tgz", - "integrity": "sha512-IUKx7JcTVbByUx1iHGFS/QsIjx7pqwTMHL9bl/NGyhyyydbfNrpruo2C7W6V4KZrbkkCOlX8QVrCoGOFW5qecg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^7.1.0", - "node-gyp-build": "^4.8.0" - }, - "peerDependencies": { - "tree-sitter": "^0.21.0" - }, - "peerDependenciesMeta": { - "tree_sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-python/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT" - }, - "node_modules/tree-sitter-rust": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/tree-sitter-rust/-/tree-sitter-rust-0.21.0.tgz", - "integrity": "sha512-unVr73YLn3VC4Qa/GF0Nk+Wom6UtI526p5kz9Rn2iZSqwIFedyCZ3e0fKCEmUJLIPGrTb/cIEdu3ZUNGzfZx7A==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^7.1.0", - "node-gyp-build": "^4.8.0" - }, - "peerDependencies": { - "tree-sitter": "^0.21.1" - }, - "peerDependenciesMeta": { - "tree_sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-rust/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT" - }, - "node_modules/tree-sitter-swift": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tree-sitter-swift/-/tree-sitter-swift-0.6.0.tgz", - "integrity": "sha512-9vOJZes4/UFjBr4COHtp6ZHVuZYwfChSQbpneXQog04dAstfx5px3ybVX2cN+ylvLqsvVpmXLpidxxgF2rDQ7A==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^8.0.0", - "node-gyp-build": "^4.8.0", - "tree-sitter-cli": "^0.23", - "which": "2.0.2" - }, - "peerDependencies": { - "tree-sitter": "^0.21.1" - }, - "peerDependenciesMeta": { - "tree_sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-swift/node_modules/node-addon-api": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", - "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", - "license": "MIT", - "optional": true, - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tree-sitter-typescript": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/tree-sitter-typescript/-/tree-sitter-typescript-0.21.2.tgz", - "integrity": "sha512-/RyNK41ZpkA8PuPZimR6pGLvNR1p0ibRUJwwQn4qAjyyLEIQD/BNlwS3NSxWtGsAWZe9gZ44VK1mWx2+eQVldg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^8.0.0", - "node-gyp-build": "^4.8.1" - }, - "peerDependencies": { - "tree-sitter": "^0.21.0" - }, - "peerDependenciesMeta": { - "tree_sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-typescript/node_modules/node-addon-api": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", - "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tree-sitter/node_modules/node-addon-api": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", - "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true - }, - "node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "license": "MIT" - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/url-join": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", - "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", - "license": "MIT" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "license": "ISC", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", - "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25 || ^4" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index d1bf3e5..0000000 --- a/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "gitnexus": "^1.4.1" - } -} diff --git a/server/.env.template b/server/.env.template index f78a16d..52e80be 100644 --- a/server/.env.template +++ b/server/.env.template @@ -43,4 +43,8 @@ # Vygenerovat pomocí: npx web-push generate-vapid-keys # VAPID_PUBLIC_KEY= # VAPID_PRIVATE_KEY= -# VAPID_SUBJECT=mailto:admin@example.com \ No newline at end of file +# VAPID_SUBJECT=mailto:admin@example.com + +# Heslo pro bypass rate limitu na endpointu /api/food/refresh (pro skripty/admin). +# Bez hesla může refresh volat každý přihlášený uživatel (podléhá rate limitu). +# REFRESH_BYPASS_PASSWORD= \ No newline at end of file diff --git a/server/.gitignore b/server/.gitignore index bf2c19e..3e0190c 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -2,6 +2,7 @@ /dist /resources/easterEggs /src/gen +/coverage .env.production .env.development .easter-eggs.json diff --git a/server/changelogs/2025-01-07.json b/server/changelogs/2025-01-07.json new file mode 100644 index 0000000..fe91c5a --- /dev/null +++ b/server/changelogs/2025-01-07.json @@ -0,0 +1,4 @@ +[ + "Zimní atmosféra", + "Skrytí podniku U Motlíků" +] \ No newline at end of file diff --git a/server/changelogs/2025-01-15.json b/server/changelogs/2025-01-15.json new file mode 100644 index 0000000..db53a68 --- /dev/null +++ b/server/changelogs/2025-01-15.json @@ -0,0 +1,3 @@ +[ + "Přidání restaurace Zastávka u Michala" +] \ No newline at end of file diff --git a/server/changelogs/2025-01-29.json b/server/changelogs/2025-01-29.json new file mode 100644 index 0000000..91096c8 --- /dev/null +++ b/server/changelogs/2025-01-29.json @@ -0,0 +1,3 @@ +[ + "Přidání restaurace Pivovarský šenk Šeříková" +] \ No newline at end of file diff --git a/server/changelogs/2025-02-19.json b/server/changelogs/2025-02-19.json new file mode 100644 index 0000000..0180d36 --- /dev/null +++ b/server/changelogs/2025-02-19.json @@ -0,0 +1,3 @@ +[ + "Možnost výběru podniku/jídla kliknutím" +] \ No newline at end of file diff --git a/server/changelogs/2025-02-27.json b/server/changelogs/2025-02-27.json new file mode 100644 index 0000000..2c4cbb1 --- /dev/null +++ b/server/changelogs/2025-02-27.json @@ -0,0 +1,3 @@ +[ + "Stránka se statistikami nejoblíbenějších voleb" +] \ No newline at end of file diff --git a/server/changelogs/2025-03-05.json b/server/changelogs/2025-03-05.json new file mode 100644 index 0000000..a50fa16 --- /dev/null +++ b/server/changelogs/2025-03-05.json @@ -0,0 +1,3 @@ +[ + "Zobrazení počtu osob u každé volby" +] \ No newline at end of file diff --git a/server/changelogs/2025-03-20.json b/server/changelogs/2025-03-20.json new file mode 100644 index 0000000..d896fab --- /dev/null +++ b/server/changelogs/2025-03-20.json @@ -0,0 +1,3 @@ +[ + "Migrace na generované OpenApi" +] \ No newline at end of file diff --git a/server/changelogs/2025-03-21.json b/server/changelogs/2025-03-21.json new file mode 100644 index 0000000..ed08c3a --- /dev/null +++ b/server/changelogs/2025-03-21.json @@ -0,0 +1,3 @@ +[ + "Odebrání zimní atmosféry" +] \ No newline at end of file diff --git a/server/changelogs/2025-08-01.json b/server/changelogs/2025-08-01.json new file mode 100644 index 0000000..a8d8e50 --- /dev/null +++ b/server/changelogs/2025-08-01.json @@ -0,0 +1,3 @@ +[ + "Možnost ručního přenačtení menu" +] \ No newline at end of file diff --git a/server/changelogs/2025-10-06.json b/server/changelogs/2025-10-06.json new file mode 100644 index 0000000..89b9306 --- /dev/null +++ b/server/changelogs/2025-10-06.json @@ -0,0 +1,3 @@ +[ + "Parsování a zobrazení alergenů" +] \ No newline at end of file diff --git a/server/changelogs/2025-10-11.json b/server/changelogs/2025-10-11.json new file mode 100644 index 0000000..f3a5ff6 --- /dev/null +++ b/server/changelogs/2025-10-11.json @@ -0,0 +1,4 @@ +[ + "Oddělení přenačtení menu do vlastního dialogu", + "Podzimní atmosféra" +] \ No newline at end of file diff --git a/server/changelogs/2025-11-03.json b/server/changelogs/2025-11-03.json new file mode 100644 index 0000000..f7912d7 --- /dev/null +++ b/server/changelogs/2025-11-03.json @@ -0,0 +1,3 @@ +[ + "Možnost převzetí poznámky ostatních uživatelů" +] \ No newline at end of file diff --git a/server/changelogs/2026-01-09.json b/server/changelogs/2026-01-09.json new file mode 100644 index 0000000..44fb7a9 --- /dev/null +++ b/server/changelogs/2026-01-09.json @@ -0,0 +1,3 @@ +[ + "Zimní atmosféra" +] \ No newline at end of file diff --git a/server/changelogs/2026-01-13.json b/server/changelogs/2026-01-13.json new file mode 100644 index 0000000..a2160d8 --- /dev/null +++ b/server/changelogs/2026-01-13.json @@ -0,0 +1,3 @@ +[ + "Možnost označit se jako objednávající u volby \"Budu objednávat\"" +] \ No newline at end of file diff --git a/server/changelogs/2026-01-30.json b/server/changelogs/2026-01-30.json new file mode 100644 index 0000000..26802eb --- /dev/null +++ b/server/changelogs/2026-01-30.json @@ -0,0 +1,3 @@ +[ + "Podpora dark mode" +] \ No newline at end of file diff --git a/server/changelogs/2026-02-04.json b/server/changelogs/2026-02-04.json new file mode 100644 index 0000000..786ec34 --- /dev/null +++ b/server/changelogs/2026-02-04.json @@ -0,0 +1,7 @@ +[ + "Redesign aplikace pomocí Claude Code", + "Zobrazení uplynulého týdne i o víkendu", + "Podpora Discord, ntfy a Teams notifikací (v Nastavení)", + "Trvalé zobrazení QR kódů do ručního zavření", + "Zobrazení nejvíce požadovaných funkcí (na stránce Statistiky)" +] \ No newline at end of file diff --git a/server/changelogs/2026-02-10.json b/server/changelogs/2026-02-10.json new file mode 100644 index 0000000..4b2412b --- /dev/null +++ b/server/changelogs/2026-02-10.json @@ -0,0 +1,3 @@ +[ + "Zobrazení sekce Pizza day pouze při volbě \"Pizza day\"" +] \ No newline at end of file diff --git a/server/changelogs/2026-02-20.json b/server/changelogs/2026-02-20.json new file mode 100644 index 0000000..dd74593 --- /dev/null +++ b/server/changelogs/2026-02-20.json @@ -0,0 +1,3 @@ +[ + "Možnost generování obecných QR kódů pro platby i mimo Pizza day (v uživatelském menu)" +] \ No newline at end of file diff --git a/server/changelogs/2026-03-04.json b/server/changelogs/2026-03-04.json new file mode 100644 index 0000000..f24dc74 --- /dev/null +++ b/server/changelogs/2026-03-04.json @@ -0,0 +1,3 @@ +[ + "Podpora push notifikací pro připomenutí výběru oběda (v nastavení)" +] \ No newline at end of file diff --git a/server/changelogs/2026-03-05.json b/server/changelogs/2026-03-05.json new file mode 100644 index 0000000..c58093a --- /dev/null +++ b/server/changelogs/2026-03-05.json @@ -0,0 +1,3 @@ +[ + "Oprava detekce zastaralého menu" +] \ No newline at end of file diff --git a/server/changelogs/2026-03-08.json b/server/changelogs/2026-03-08.json new file mode 100644 index 0000000..70ae24f --- /dev/null +++ b/server/changelogs/2026-03-08.json @@ -0,0 +1,3 @@ +[ + "Automatické zobrazení dialogu s dosud nezobrazenými novinkami" +] \ No newline at end of file diff --git a/server/changelogs/2026-03-09.json b/server/changelogs/2026-03-09.json new file mode 100644 index 0000000..216ac8d --- /dev/null +++ b/server/changelogs/2026-03-09.json @@ -0,0 +1,3 @@ +[ + "Automatický výběr výchozího času preferovaného odchodu" +] \ No newline at end of file diff --git a/server/jest.config.js b/server/jest.config.js new file mode 100644 index 0000000..a7fb576 --- /dev/null +++ b/server/jest.config.js @@ -0,0 +1,6 @@ +module.exports = { + testEnvironment: 'node', + testMatch: ['/src/**/*.test.ts'], + testPathIgnorePatterns: ['/node_modules/', '/dist/'], + setupFiles: ['/src/tests/setupEnv.ts'], +}; diff --git a/server/package.json b/server/package.json index 0b0ccfb..e40af6b 100644 --- a/server/package.json +++ b/server/package.json @@ -19,10 +19,12 @@ "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.10.0", "@types/request-promise": "^4.1.48", + "@types/supertest": "^6.0.0", "@types/web-push": "^3.6.4", "babel-jest": "^30.2.0", "jest": "^30.2.0", "nodemon": "^3.1.10", + "supertest": "^7.0.0", "ts-node": "^10.9.1", "typescript": "^5.9.3" }, diff --git a/server/src/chefie.ts b/server/src/chefie.ts index fe41d40..8cd50be 100644 --- a/server/src/chefie.ts +++ b/server/src/chefie.ts @@ -1,6 +1,7 @@ import axios from 'axios'; import { load } from 'cheerio'; -import { getPizzaListMock } from './mock'; +import { getPizzaListMock, getSalatListMock } from './mock'; +import { Salat } from '../../types/gen/types.gen'; // TODO přesunout do types type PizzaSize = { @@ -20,7 +21,8 @@ type Pizza = { // TODO mělo by být konfigurovatelné proměnnou z prostředí s tímhle jako default const baseUrl = 'https://www.pizzachefie.cz'; -const pizzyUrl = `${baseUrl}/pizzy.html?pobocka=plzen`; +const pizzyUrl = `${baseUrl}/pizzy.html`; +const salayUrl = `${baseUrl}/salaty.html`; const buildPizzaUrl = (pizzaUrl: string) => { return `${baseUrl}/${pizzaUrl}`; @@ -34,9 +36,12 @@ const boxPrices: { [key: string]: number } = { "50cm": 25 } +// Cena obalu pro salát +const SALAT_BOX_PRICE = 13; + /** * Stáhne a scrapne aktuální pizzy ze stránek Pizza Chefie. - * + * * @param mock zda vrátit pouze mock data */ export async function downloadPizzy(mock: boolean): Promise { @@ -84,4 +89,38 @@ export async function downloadPizzy(mock: boolean): Promise { }); } return result; +} + +/** + * Stáhne a scrapne aktuální saláty ze stránek Pizza Chefie. + * Příplatek za obal je pro každý salát pevně 13 Kč. + * + * @param mock zda vrátit pouze mock data + */ +export async function downloadSalaty(mock: boolean): Promise { + if (mock) { + return new Promise((resolve) => setTimeout(() => resolve(getSalatListMock()), 1000)); + } + const html = await axios.get(salayUrl).then(res => res.data); + const $ = load(html); + const links = $('.vypisproduktu > div > h4 > a'); + const urls = []; + for (const element of links) { + if (element.name === 'a' && element.attribs?.href) { + urls.push(buildPizzaUrl(element.attribs.href)); + } + } + const result: Salat[] = []; + for (const url of urls) { + const salatHtml = await axios.get(url).then(res => res.data); + const name = $('.produkt > h2', salatHtml).first().text().trim(); + const ingredients: string[] = []; + $('.prisady > li', salatHtml).each((i, elm) => { + ingredients.push($(elm).text()); + }); + const priceText = $('.cena > span', salatHtml).first().text().trim(); + const price = Number.parseInt(priceText.split(' Kč')[0]); + result.push({ name, ingredients, price: price + SALAT_BOX_PRICE }); + } + return result; } \ No newline at end of file diff --git a/server/src/index.ts b/server/src/index.ts index 93b7ff9..850ecfe 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -2,6 +2,7 @@ import express from "express"; import bodyParser from "body-parser"; import cors from 'cors'; import { getData, getDateForWeekIndex, getToday } from "./service"; +import { MealSlot } from "../../types/gen/types.gen"; import dotenv from 'dotenv'; import path from 'path'; import { getQr } from "./qr"; @@ -10,6 +11,7 @@ import { getIsWeekend, InsufficientPermissions, PizzaDayConflictError, parseToke import { getPendingQrs } from "./pizza"; import { initWebsocket } from "./websocket"; import { startReminderScheduler } from "./pushReminder"; +import { storageReady } from "./storage"; import pizzaDayRoutes from "./routes/pizzaDayRoutes"; import foodRoutes, { refreshMetoda } from "./routes/foodRoutes"; import votingRoutes from "./routes/votingRoutes"; @@ -18,6 +20,7 @@ import statsRoutes from "./routes/statsRoutes"; import notificationRoutes from "./routes/notificationRoutes"; import qrRoutes from "./routes/qrRoutes"; import devRoutes from "./routes/devRoutes"; +import changelogRoutes from "./routes/changelogRoutes"; const ENVIRONMENT = process.env.NODE_ENV ?? 'production'; dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) }); @@ -55,6 +58,10 @@ if (HTTP_REMOTE_USER_ENABLED) { // ----------- Metody nevyžadující token -------------- +app.get("/api/health", (_req, res) => { + res.status(200).json({ ok: true }); +}); + app.get("/api/whoami", (req, res) => { if (!HTTP_REMOTE_USER_ENABLED) { res.status(403).json({ error: 'Není zapnuté přihlášení z hlaviček' }); @@ -86,12 +93,15 @@ app.post("/api/login", (req, res) => { } }); -// TODO dočasné řešení - QR se zobrazuje přes , nemáme sem jak dostat token -app.get("/api/qr", (req, res) => { +// QR se zobrazuje přes , nemáme sem jak dostat token +app.get("/api/qr", async (req, res) => { if (!req.query?.login) { - throw Error("Nebyl předán login"); + return res.status(400).json({ error: "Nebyl předán login" }); } - const img = getQr(req.query.login as string); + if (!req.query?.id) { + return res.status(400).json({ error: "Nebyl předán identifikátor QR kódu" }); + } + const img = await getQr(req.query.login as string, req.query.id as string); res.writeHead(200, { 'Content-Type': 'image/png', 'Content-Length': img.length @@ -142,8 +152,8 @@ app.get("/api/data", async (req, res) => { // Na víkendu zobrazíme pátek místo hlášky "Užívejte víkend" date = getDateForWeekIndex(4); } - const slotParam = typeof req.query.slot === 'string' ? req.query.slot : undefined; - if (slotParam && slotParam !== 'obed' && slotParam !== 'extra') { + const slotParam = typeof req.query.slot === 'string' ? req.query.slot as MealSlot : undefined; + if (slotParam && slotParam !== MealSlot.OBED && slotParam !== MealSlot.EXTRA) { return res.status(400).json({ error: 'Neplatný slot' }); } const data = await getData(date, slotParam); @@ -169,6 +179,7 @@ app.use("/api/stats", statsRoutes); app.use("/api/notifications", notificationRoutes); app.use("/api/qr", qrRoutes); app.use("/api/dev", devRoutes); +app.use("/api/changelogs", changelogRoutes); app.use('/stats', express.static('public')); app.use(express.static('public')); @@ -188,9 +199,11 @@ app.use((err: any, req: any, res: any, next: any) => { const PORT = process.env.PORT ?? 3001; const HOST = process.env.HOST ?? '0.0.0.0'; -server.listen(PORT, () => { - console.log(`Server listening on ${HOST}, port ${PORT}`); - startReminderScheduler(); +storageReady.then(() => { + server.listen(PORT, () => { + console.log(`Server listening on ${HOST}, port ${PORT}`); + startReminderScheduler(); + }); }); // Umožníme vypnutí serveru přes SIGINT, jinak Docker čeká než ho sestřelí diff --git a/server/src/mock.ts b/server/src/mock.ts index 68ecb13..db57404 100644 --- a/server/src/mock.ts +++ b/server/src/mock.ts @@ -1429,27 +1429,46 @@ export const getPizzaListMock = () => { return MOCK_PIZZA_LIST; } +// Mockovací data pro saláty +const MOCK_SALAT_LIST = [ + { + name: "Greek", + ingredients: ["Salát", "Černé olivy", "Paprika mix", "Červená cibule", "Rajčata", "Okurka salátová", "Jogurtový dresing"], + price: 174 + 13, + }, + { + name: "Caesar", + ingredients: ["Salát", "Rajčata", "Kuřecí maso", "Krutony", "Parmazán", "Caesar dresing", "Olivový olej"], + price: 184 + 13, + }, + { + name: "Šopský salát", + ingredients: ["Salátová okurka", "Rajčata", "Paprika mix", "Červená cibule", "Balkánský sýr"], + price: 164 + 13, + }, + { + name: "Těstovinový salát", + ingredients: ["Penne", "Okurka", "Rajčata", "Paprika mix", "Kuřecí maso", "Jogurtový dresing"], + price: 184 + 13, + }, +] + +export const getSalatListMock = () => { + return MOCK_SALAT_LIST; +} + export const getStatsMock = (): WeeklyStats => { + const mkDay = (date: string, di: number) => ({ + date, + locations: Object.keys(LunchChoice).reduce((prev, cur, ci) => ( + { ...prev, [cur]: (di * 7 + ci * 3) % 10 } + ), {} as Record), + }); return [ - { - date: '24.02.', - locations: { ...Object.keys(LunchChoice).reduce((prev, cur) => ({ ...prev, [cur]: Math.floor(Math.random() * 10) }), {}) } - }, - { - date: '25.02.', - locations: { ...Object.keys(LunchChoice).reduce((prev, cur) => ({ ...prev, [cur]: Math.floor(Math.random() * 10) }), {}) } - }, - { - date: '26.02.', - locations: { ...Object.keys(LunchChoice).reduce((prev, cur) => ({ ...prev, [cur]: Math.floor(Math.random() * 10) }), {}) } - }, - { - date: '27.02.', - locations: { ...Object.keys(LunchChoice).reduce((prev, cur) => ({ ...prev, [cur]: Math.floor(Math.random() * 10) }), {}) } - }, - { - date: '28.02.', - locations: { ...Object.keys(LunchChoice).reduce((prev, cur) => ({ ...prev, [cur]: Math.floor(Math.random() * 10) }), {}) } - } + mkDay('24.02.', 0), + mkDay('25.02.', 1), + mkDay('26.02.', 2), + mkDay('27.02.', 3), + mkDay('28.02.', 4), ]; } \ No newline at end of file diff --git a/server/src/pizza.ts b/server/src/pizza.ts index a9434c8..640c8bf 100644 --- a/server/src/pizza.ts +++ b/server/src/pizza.ts @@ -2,9 +2,10 @@ import { formatDate } from "./utils"; import { callNotifikace } from "./notifikace"; import { generateQr } from "./qr"; import getStorage from "./storage"; -import { downloadPizzy } from "./chefie"; +import { downloadPizzy, downloadSalaty } from "./chefie"; import { getClientData, getToday, initIfNeeded } from "./service"; -import { Pizza, ClientData, PizzaDayState, PizzaSize, PizzaOrder, PizzaVariant, UdalostEnum, PendingQr } from "../../types/gen/types.gen"; +import { Pizza, Salat, ClientData, PizzaDayState, PizzaSize, PizzaOrder, PizzaVariant, UdalostEnum, PendingQr } from "../../types/gen/types.gen"; +import crypto from "crypto"; const storage = getStorage(); const PENDING_QR_PREFIX = 'pending_qr'; @@ -25,7 +26,7 @@ export async function getPizzaList(): Promise { /** * Uloží seznam dostupných pizz pro dnešní den. - * + * * @param pizzaList seznam dostupných pizz */ export async function savePizzaList(pizzaList: Pizza[]): Promise { @@ -38,6 +39,34 @@ export async function savePizzaList(pizzaList: Pizza[]): Promise { return clientData; } +/** + * Vrátí seznam dostupných salátů pro dnešní den. + * Stáhne je, pokud je pro dnešní den nemá. + */ +export async function getSalatList(): Promise { + await initIfNeeded(); + let clientData = await getClientData(getToday()); + if (!clientData.salatList) { + const mock = process.env.MOCK_DATA === 'true'; + clientData = await saveSalatList(await downloadSalaty(mock)); + } + return Promise.resolve(clientData.salatList); +} + +/** + * Uloží seznam dostupných salátů pro dnešní den. + * + * @param salatList seznam dostupných salátů + */ +export async function saveSalatList(salatList: Salat[]): Promise { + await initIfNeeded(); + const today = formatDate(getToday()); + const clientData = await getClientData(getToday()); + clientData.salatList = salatList; + await storage.setData(today, clientData); + return clientData; +} + /** * Vytvoří pizza day pro aktuální den a vrátí data pro klienta. */ @@ -48,8 +77,8 @@ export async function createPizzaDay(creator: string): Promise { throw Error("Pizza day pro dnešní den již existuje"); } // TODO berka rychlooprava, vyřešit lépe - stahovat jednou, na jediném místě! - const pizzaList = await getPizzaList(); - const data: ClientData = { pizzaDay: { state: PizzaDayState.CREATED, creator, orders: [] }, pizzaList, ...clientData }; + const [pizzaList, salatList] = await Promise.all([getPizzaList(), getSalatList()]); + const data: ClientData = { pizzaDay: { state: PizzaDayState.CREATED, creator, orders: [] }, pizzaList, salatList, ...clientData }; const today = formatDate(getToday()); await storage.setData(today, data); callNotifikace({ input: { udalost: UdalostEnum.ZAHAJENA_PIZZA, user: creator } }) @@ -113,6 +142,46 @@ export async function addPizzaOrder(login: string, pizza: Pizza, size: PizzaSize return clientData; } +/** + * Přidá objednávku salátu uživateli. + * + * @param login login uživatele + * @param salat zvolený salát + */ +export async function addSalatOrder(login: string, salat: Salat) { + const today = formatDate(getToday()); + const clientData = await getClientData(getToday()); + if (!clientData.pizzaDay) { + throw Error("Pizza day pro dnešní den neexistuje"); + } + if (clientData.pizzaDay.state !== PizzaDayState.CREATED) { + throw Error("Pizza day není ve stavu " + PizzaDayState.CREATED); + } + let order: PizzaOrder | undefined = clientData.pizzaDay?.orders?.find(o => o.customer === login); + if (!order) { + order = { + customer: login, + pizzaList: [], + totalPrice: 0, + hasQr: false, + } + clientData.pizzaDay.orders ??= []; + clientData.pizzaDay.orders.push(order); + } + const salatOrder: PizzaVariant = { + varId: 0, + name: salat.name, + size: "1 porce", + price: salat.price, + category: 'salat', + } + order.pizzaList ??= []; + order.pizzaList.push(salatOrder); + order.totalPrice += salatOrder.price; + await storage.setData(today, clientData); + return clientData; +} + /** * Odstraní všechny pizzy uživatele (celou jeho objednávku). * Pokud Pizza day neexistuje nebo není ve stavu CREATED, neudělá nic. @@ -269,11 +338,15 @@ export async function finishPizzaDelivery(login: string, bankAccount?: string, b if (bankAccount?.length && bankAccountHolder?.length) { for (const order of clientData.pizzaDay.orders!) { if (order.customer !== login) { // zatím platí creator = objednávající, a pro toho nemá QR kód smysl - let message = order.pizzaList!.map(pizza => `Pizza ${pizza.name} (${pizza.size})`).join(', '); - await generateQr(order.customer, bankAccount, bankAccountHolder, order.totalPrice, message); + const id = crypto.randomUUID(); + let message = order.pizzaList!.map(item => + item.category === 'salat' ? `Salát ${item.name}` : `Pizza ${item.name} (${item.size})` + ).join(', '); + await generateQr(order.customer, bankAccount, bankAccountHolder, order.totalPrice, message, id); order.hasQr = true; // Uložíme nevyřízený QR kód pro persistentní zobrazení await addPendingQr(order.customer, { + id, date: today, creator: login, totalPrice: order.totalPrice, @@ -360,8 +433,8 @@ function getPendingQrKey(login: string): string { export async function addPendingQr(login: string, pendingQr: PendingQr): Promise { const key = getPendingQrKey(login); const existing = await storage.getData(key) ?? []; - // Nepřidáváme duplicity pro stejný den - if (!existing.some(qr => qr.date === pendingQr.date)) { + // Deduplikace podle id (ne podle data — jeden den může mít uživatel víc QR kódů) + if (!existing.some(qr => qr.id === pendingQr.id)) { existing.push(pendingQr); await storage.setData(key, existing); } @@ -375,11 +448,11 @@ export async function getPendingQrs(login: string): Promise { } /** - * Označí QR kód pro daný den jako uhrazený (odstraní ho ze seznamu nevyřízených). + * Označí QR kód jako uhrazený (odstraní ho ze seznamu nevyřízených). */ -export async function dismissPendingQr(login: string, date: string): Promise { +export async function dismissPendingQr(login: string, id: string): Promise { const key = getPendingQrKey(login); const existing = await storage.getData(key) ?? []; - const filtered = existing.filter(qr => qr.date !== date); + const filtered = existing.filter(qr => qr.id !== id); await storage.setData(key, filtered); } \ No newline at end of file diff --git a/server/src/qr.ts b/server/src/qr.ts index 2493bc1..6327972 100644 --- a/server/src/qr.ts +++ b/server/src/qr.ts @@ -1,22 +1,20 @@ -import fs from "fs"; import axios from "axios"; -import os from "os"; -import path from "path"; import crypto from "crypto"; -import { formatDate } from "./utils"; +import getStorage from "./storage"; const QR_GENERATOR_URL = 'https://api.paylibo.com/paylibo/generator/image'; const COUNTRY_CODE = 'CZ'; const CURRENCY_CODE = 'CZK'; const QR_PIXEL_SIZE = 256; -const tmpDir = os.tmpdir(); + +const storage = getStorage(); /** * Převede číslo účtu z BBAN do IBAN. Automaticky dopočítá kontrolní číslice. - * + * * @param bankAccountNumber číslo účtu ve formátu BBAN (123456-0123456789/0100) */ -function convertBbanToIban(bankAccountNumber: string): string { +export function convertBbanToIban(bankAccountNumber: string): string { // TODO validovat číslo účtu stejně jako na klientovi, pro případ že sem někdo pošle nesmysl let prefix: string = ''; let accountNumber: string = bankAccountNumber; @@ -41,29 +39,26 @@ function convertBbanToIban(bankAccountNumber: string): string { return iban; } -function createNameHash(customerName: string): string { - return crypto.createHash('md5').update(customerName).digest('hex'); -} - -function createFilePath(nameHash: string): string { - const fileName = `${formatDate(new Date())}_${nameHash}.png`; - return path.join(tmpDir, fileName); +function createStorageKey(customerName: string, id: string): string { + const nameHash = crypto.createHash('md5').update(customerName).digest('hex'); + return `qr_${nameHash}_${id}`; } /** - * Vygeneruje, uloží a vrátí unikátní ID obrázku platebního QR kódu s danými parametry. - * + * Vygeneruje a uloží obrázek platebního QR kódu do storage (Redis/JSON). + * Data přežijí redeploy — není třeba persistentní filesystém. + * * @param customerName jméno uživatele, pro kterého je QR kód generován * @param bankAccountNumber číslo cílového bankovního účtu ve formátu BBAN * @param bankAccountHolder jméno držitele cílového bankovního účtu * @param amount částka v Kč * @param message zpráva pro příjemce - * @returns hash, pomocí kterého lze následně získat vygenerovaný obrázek + * @param id unikátní identifikátor (UUID) tohoto QR kódu */ -export async function generateQr(customerName: string, bankAccountNumber: string, bankAccountHolder: string, amount: number, message: string): Promise { +export async function generateQr(customerName: string, bankAccountNumber: string, bankAccountHolder: string, amount: number, message: string, id: string): Promise { // Zpráva pro příjemce nesmí dle standardu obsahovat '*' a být delší než 60 znaků if (message.indexOf('*') >= 0) { - message = message.replace('*', ''); + message = message.replace(/\*/g, ''); } if (message.length > 60) { message = message.substring(0, 60); @@ -77,22 +72,23 @@ export async function generateQr(customerName: string, bankAccountNumber: string branding: false, compress: false, size: QR_PIXEL_SIZE, - } - const response = await axios.get(QR_GENERATOR_URL, { responseType: 'stream', params: { ...payload } }); - // Použijeme hash, abychom nemuseli řešit nepovolené znaky ve jménu uživatele - const nameHash = createNameHash(customerName); - const imgPath = createFilePath(nameHash); - response.data.pipe(fs.createWriteStream(imgPath)); - return nameHash; + }; + const response = await axios.get(QR_GENERATOR_URL, { responseType: 'arraybuffer', params: { ...payload } }); + const base64 = Buffer.from(response.data).toString('base64'); + await storage.setData(createStorageKey(customerName, id), base64); } /** - * Vrátí obrázek s QR kódem, pokud existuje. - * + * Vrátí obrázek s QR kódem ze storage. + * * @param customerName jméno uživatele + * @param id unikátní identifikátor QR kódu * @returns data obrázku */ -export function getQr(customerName: string): Buffer { - const imgPath = createFilePath(createNameHash(customerName)); - return fs.readFileSync(imgPath); -} \ No newline at end of file +export async function getQr(customerName: string, id: string): Promise { + const base64 = await storage.getData(createStorageKey(customerName, id)); + if (!base64) { + throw new Error("QR kód nebyl nalezen"); + } + return Buffer.from(base64, 'base64'); +} diff --git a/server/src/restaurants.ts b/server/src/restaurants.ts index 7cae72e..2eb0d1e 100644 --- a/server/src/restaurants.ts +++ b/server/src/restaurants.ts @@ -4,6 +4,10 @@ import { getMenuSladovnickaMock, getMenuTechTowerMock, getMenuUMotlikuMock, getM import { formatDate } from "./utils"; import { Food } from "../../types/gen/types.gen"; +export class StaleWeekError extends Error { + constructor(public food: Food[][]) { super('Data jsou z jiného týdne'); } +} + // Fráze v názvech jídel, které naznačují že se jedná o polévku const SOUP_NAMES = [ 'polévka', @@ -36,7 +40,7 @@ const SENKSERIKOVA_URL = 'https://www.menicka.cz/6561-pivovarsky-senk-serikova.h * @param text vstupní text * @returns true, pokud text představuje polévku */ -const isTextSoupName = (text: string): boolean => { +export const isTextSoupName = (text: string): boolean => { for (const name of SOUP_NAMES) { if (text.toLowerCase().includes(name)) { return true; @@ -45,11 +49,11 @@ const isTextSoupName = (text: string): boolean => { return false; } -const capitalize = (word: string): string => { +export const capitalize = (word: string): string => { return word.charAt(0).toUpperCase() + word.slice(1); } -const sanitizeText = (text: string): string => { +export const sanitizeText = (text: string): string => { return text.replace('\t', '').replace(' , ', ', ').trim(); } @@ -60,7 +64,7 @@ const sanitizeText = (text: string): string => { * @param name původní název jídla * @returns objekt obsahující vyčištěný název a pole alergenů */ -const parseAllergens = (name: string): { cleanName: string, allergens: number[] } => { +export const parseAllergens = (name: string): { cleanName: string, allergens: number[] } => { // Regex pro nalezení čísel na konci řetězce oddělených čárkami a případnými mezerami const regex = /\s+(\d+(?:\s*,\s*\d+)*)\s*$/; const match = regex.exec(name); @@ -276,6 +280,7 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal const $ = load(html); let secondTry = false; + let thirdTry = false; // První pokus - varianta "Obědy" let fonts = $('font.wsw-41'); let font = undefined; @@ -284,7 +289,7 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal font = f; } }) - // Druhý pokus - varianta "Jídelní lístek" + // Druhý pokus - varianta "Jídelní lístek" (starší formát) if (!font) { fonts = $('font.wnd-font-size-90'); fonts.each((i, f) => { @@ -294,13 +299,26 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal } }) } + // Třetí pokus - nový formát: font.wsw-41 s textem "Jídelní lístek" (vše v jednom bloku) + if (!font) { + fonts = $('font.wsw-41'); + fonts.each((i, f) => { + if ($(f).text().trim().startsWith('Jídelní lístek')) { + font = f; + thirdTry = true; + } + }) + } if (!font) { throw new Error('Chyba: nenalezen pro obědy v HTML Techtower.'); } const result: Food[][] = []; - // TODO validovat, že v textu nalezeného je rozsah, do kterého spadá vstupní datum - const siblings = secondTry ? $(font).parent().parent().parent().siblings('p') : $(font).parent().parent().siblings(); + const siblings = thirdTry + ? $(font).parent().siblings('p') + : secondTry + ? $(font).parent().parent().parent().siblings('p') + : $(font).parent().parent().siblings(); let parsing = false; let currentDayIndex = 0; for (let i = 0; i < siblings.length; i++) { @@ -345,6 +363,18 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal }) } } + + // Validace, zda text hlavičky obsahuje datum odpovídající požadovanému týdnu + const headerText = $(font).text().trim(); + const dateMatch = headerText.match(/(\d{1,2})\.(\d{1,2})\./); + if (dateMatch) { + const foundDay = parseInt(dateMatch[1]); + const foundMonth = parseInt(dateMatch[2]) - 1; // JS months are 0-based + if (foundDay !== firstDayOfWeek.getDate() || foundMonth !== firstDayOfWeek.getMonth()) { + throw new StaleWeekError(result); + } + } + return result; } diff --git a/server/src/routes/changelogRoutes.ts b/server/src/routes/changelogRoutes.ts new file mode 100644 index 0000000..20598fd --- /dev/null +++ b/server/src/routes/changelogRoutes.ts @@ -0,0 +1,50 @@ +import express, { Request, Response } from "express"; +import fs from "fs"; +import path from "path"; + +const router = express.Router(); + +const CHANGELOGS_DIR = path.resolve(__dirname, "../../changelogs"); + +// In-memory cache: datum → seznam změn +const cache: Record = {}; + +function loadAllChangelogs(): Record { + let files: string[]; + try { + files = fs.readdirSync(CHANGELOGS_DIR).filter(f => f.endsWith(".json")); + } catch { + return {}; + } + + for (const file of files) { + const date = file.replace(".json", ""); + if (!cache[date]) { + const content = fs.readFileSync(path.join(CHANGELOGS_DIR, file), "utf-8"); + cache[date] = JSON.parse(content); + } + } + + return cache; +} + +router.get("/", (req: Request, res: Response) => { + const all = loadAllChangelogs(); + const since = typeof req.query.since === "string" ? req.query.since : undefined; + + // Seřazení od nejnovějšího po nejstarší + const sortedDates = Object.keys(all).sort((a, b) => b.localeCompare(a)); + + const filteredDates = since + ? sortedDates.filter(date => date > since) + : sortedDates; + + const result: Record = {}; + for (const date of filteredDates) { + result[date] = all[date]; + } + + res.status(200).json(result); +}); + +export default router; diff --git a/server/src/routes/devRoutes.ts b/server/src/routes/devRoutes.ts index b717b5f..97999cc 100644 --- a/server/src/routes/devRoutes.ts +++ b/server/src/routes/devRoutes.ts @@ -42,7 +42,7 @@ const RESTAURANTS_WITH_MENU = [ * Middleware pro kontrolu DEV režimu */ function requireDevMode(req: any, res: any, next: any) { - if (ENVIRONMENT !== 'development') { + if (ENVIRONMENT !== 'development' && ENVIRONMENT !== 'test') { return res.status(403).json({ error: 'Tento endpoint je dostupný pouze ve vývojovém režimu' }); } next(); @@ -141,8 +141,9 @@ router.post("/clear", async (req: Request<{}, any, any>, res, next) => { const dateKey = formatDate(date); const data = await storage.getData(dateKey); - // Vymažeme všechny volby + // Vymažeme všechny volby i aktivní pizza day data.choices = {}; + delete data.pizzaDay; await storage.setData(dateKey, data); diff --git a/server/src/routes/foodRoutes.ts b/server/src/routes/foodRoutes.ts index 97eae55..54ae702 100644 --- a/server/src/routes/foodRoutes.ts +++ b/server/src/routes/foodRoutes.ts @@ -4,7 +4,7 @@ import { addChoice, getDateForWeekIndex, getToday, removeChoice, removeChoices, import { getDayOfWeekIndex, parseToken, getFirstWorkDayOfWeek } from "../utils"; import { getWebsocket } from "../websocket"; import { callNotifikace } from "../notifikace"; -import { AddChoiceData, ChangeDepartureTimeData, RemoveChoiceData, RemoveChoicesData, UdalostEnum, UpdateNoteData } from "../../../types/gen/types.gen"; +import { AddChoiceData, ChangeDepartureTimeData, MealSlot, RemoveChoiceData, RemoveChoicesData, UdalostEnum, UpdateNoteData } from "../../../types/gen/types.gen"; // RateLimit na refresh endpoint @@ -69,9 +69,9 @@ const parseValidateFutureDayIndex = (req: Request<{}, any, AddChoiceData["body"] return dayIndex; } -const parseSlot = (body: Record): string | undefined => { +const parseSlot = (body: Record): MealSlot | undefined => { const slot = body?.slot; - if (slot != null && slot !== 'obed' && slot !== 'extra') { + if (slot != null && slot !== MealSlot.OBED && slot !== MealSlot.EXTRA) { throw Error(`Neplatný slot: ${slot}`); } return slot ?? undefined; @@ -204,13 +204,20 @@ router.post("/updateBuyer", async (req, res, next) => { } catch (e: any) { next(e) } }); -// /api/food/refresh?type=week&heslo=docasnyheslo +// /api/food/refresh?type=week (přihlášený uživatel, nebo ?heslo=... pro bypass rate limitu) export const refreshMetoda = async (req: Request, res: Response) => { const { type, heslo } = req.query as { type?: string; heslo?: string }; - if (heslo !== "docasnyheslo" && heslo !== "tohleheslopavelnesmizjistit123") { - return res.status(403).json({ error: "Neplatné heslo" }); + const bypassPassword = process.env.REFRESH_BYPASS_PASSWORD; + const isBypass = !!bypassPassword && heslo === bypassPassword; + + if (!isBypass) { + try { + getLogin(parseToken(req)); + } catch { + return res.status(403).json({ error: "Přihlaste se prosím" }); + } } - if (!checkRateLimit("refresh") && heslo !== "tohleheslopavelnesmizjistit123") { + if (!checkRateLimit("refresh") && !isBypass) { return res.status(429).json({ error: "Refresh už se zavolal, chvíli počkej :))" }); } if (type !== "week" && type !== "day") { diff --git a/server/src/routes/pizzaDayRoutes.ts b/server/src/routes/pizzaDayRoutes.ts index 82a26c5..e6182a5 100644 --- a/server/src/routes/pizzaDayRoutes.ts +++ b/server/src/routes/pizzaDayRoutes.ts @@ -1,6 +1,6 @@ import express, { Request } from "express"; import { getLogin } from "../auth"; -import { createPizzaDay, deletePizzaDay, getPizzaList, addPizzaOrder, removePizzaOrder, lockPizzaDay, unlockPizzaDay, finishPizzaOrder, finishPizzaDelivery, updatePizzaDayNote, updatePizzaFee, dismissPendingQr } from "../pizza"; +import { createPizzaDay, deletePizzaDay, getPizzaList, getSalatList, addPizzaOrder, addSalatOrder, removePizzaOrder, lockPizzaDay, unlockPizzaDay, finishPizzaOrder, finishPizzaDelivery, updatePizzaDayNote, updatePizzaFee, dismissPendingQr } from "../pizza"; import { parseToken } from "../utils"; import { getWebsocket } from "../websocket"; import { AddPizzaData, DismissQrData, FinishDeliveryData, RemovePizzaData, UpdatePizzaDayNoteData, UpdatePizzaFeeData } from "../../../types"; @@ -24,27 +24,43 @@ router.post("/delete", async (req: Request<{}, any, undefined>, res) => { router.post("/add", async (req: Request<{}, any, AddPizzaData["body"]>, res) => { const login = getLogin(parseToken(req)); - if (isNaN(req.body?.pizzaIndex)) { - throw Error("Nebyl předán index pizzy"); + if (req.body?.salatIndex !== undefined && !isNaN(req.body.salatIndex)) { + // Přidání salátu + const salatIndex = req.body.salatIndex; + const salaty = await getSalatList(); + if (!salaty) { + throw Error("Selhalo získání seznamu dostupných salátů."); + } + if (!salaty[salatIndex]) { + throw Error("Neplatný index salátu: " + salatIndex); + } + const data = await addSalatOrder(login, salaty[salatIndex]); + getWebsocket().emit("message", data); + res.status(200).json({}); + } else { + // Přidání pizzy + if (req.body?.pizzaIndex === undefined || isNaN(req.body.pizzaIndex)) { + throw Error("Nebyl předán index pizzy ani salátu"); + } + const pizzaIndex = req.body.pizzaIndex; + if (req.body?.pizzaSizeIndex === undefined || isNaN(req.body.pizzaSizeIndex)) { + throw Error("Nebyl předán index velikosti pizzy"); + } + const pizzaSizeIndex = req.body.pizzaSizeIndex; + let pizzy = await getPizzaList(); + if (!pizzy) { + throw Error("Selhalo získání seznamu dostupných pizz."); + } + if (!pizzy[pizzaIndex]) { + throw Error("Neplatný index pizzy: " + pizzaIndex); + } + if (!pizzy[pizzaIndex].sizes[pizzaSizeIndex]) { + throw Error("Neplatný index velikosti pizzy: " + pizzaSizeIndex); + } + const data = await addPizzaOrder(login, pizzy[pizzaIndex], pizzy[pizzaIndex].sizes[pizzaSizeIndex]); + getWebsocket().emit("message", data); + res.status(200).json({}); } - const pizzaIndex = req.body.pizzaIndex; - if (isNaN(req.body?.pizzaSizeIndex)) { - throw Error("Nebyl předán index velikosti pizzy"); - } - const pizzaSizeIndex = req.body.pizzaSizeIndex; - let pizzy = await getPizzaList(); - if (!pizzy) { - throw Error("Selhalo získání seznamu dostupných pizz."); - } - if (!pizzy[pizzaIndex]) { - throw Error("Neplatný index pizzy: " + pizzaIndex); - } - if (!pizzy[pizzaIndex].sizes[pizzaSizeIndex]) { - throw Error("Neplatný index velikosti pizzy: " + pizzaSizeIndex); - } - const data = await addPizzaOrder(login, pizzy[pizzaIndex], pizzy[pizzaIndex].sizes[pizzaSizeIndex]); - getWebsocket().emit("message", data); - res.status(200).json({}); }); router.post("/remove", async (req: Request<{}, any, RemovePizzaData["body"]>, res) => { @@ -112,11 +128,11 @@ router.post("/updatePizzaFee", async (req: Request<{}, any, UpdatePizzaFeeData[" /** Označí QR kód jako uhrazený. */ router.post("/dismissQr", async (req: Request<{}, any, DismissQrData["body"]>, res, next) => { const login = getLogin(parseToken(req)); - if (!req.body.date) { - return res.status(400).json({ error: "Nebyl předán datum" }); + if (!req.body.id) { + return res.status(400).json({ error: "Nebyl předán identifikátor QR kódu" }); } try { - await dismissPendingQr(login, req.body.date); + await dismissPendingQr(login, req.body.id); res.status(200).json({}); } catch (e: any) { next(e) } }); diff --git a/server/src/routes/qrRoutes.ts b/server/src/routes/qrRoutes.ts index e89a217..e00a69c 100644 --- a/server/src/routes/qrRoutes.ts +++ b/server/src/routes/qrRoutes.ts @@ -4,6 +4,7 @@ import { parseToken, formatDate } from "../utils"; import { generateQr } from "../qr"; import { addPendingQr } from "../pizza"; import { GenerateQrData } from "../../../types"; +import crypto from "crypto"; const router = express.Router(); @@ -44,10 +45,12 @@ router.post("/generate", async (req: Request<{}, any, GenerateQrData["body"]>, r } // Vygenerovat QR kód - await generateQr(recipient.login, bankAccount, bankAccountHolder, recipient.amount, recipient.purpose); + const id = crypto.randomUUID(); + await generateQr(recipient.login, bankAccount, bankAccountHolder, recipient.amount, recipient.purpose, id); // Uložit jako nevyřízený QR kód await addPendingQr(recipient.login, { + id, date: today, creator: login, totalPrice: recipient.amount, diff --git a/server/src/service.ts b/server/src/service.ts index 900226a..a30cf26 100644 --- a/server/src/service.ts +++ b/server/src/service.ts @@ -1,6 +1,6 @@ import { InsufficientPermissions, PizzaDayConflictError, formatDate, getDayOfWeekIndex, getFirstWorkDayOfWeek, getHumanDate, getIsWeekend, getWeekNumber } from "./utils"; import getStorage from "./storage"; -import { getMenuSladovnicka, getMenuTechTower, getMenuZastavkaUmichala, getMenuSenkSerikova } from "./restaurants"; +import { getMenuSladovnicka, getMenuTechTower, getMenuZastavkaUmichala, getMenuSenkSerikova, StaleWeekError } from "./restaurants"; import { getTodayMock } from "./mock"; import { removeAllUserPizzas } from "./pizza"; import { ClientData, DepartureTime, LunchChoice, MealSlot, PizzaDayState, Restaurant, RestaurantDayMenu, WeekMenu } from "../../types/gen/types.gen"; @@ -8,9 +8,9 @@ import { ClientData, DepartureTime, LunchChoice, MealSlot, PizzaDayState, Restau const storage = getStorage(); const MENU_PREFIX = 'menu'; -function getDataKey(date: Date, slot?: string): string { +function getDataKey(date: Date, slot?: MealSlot): string { const base = formatDate(date); - return slot === 'extra' ? `${base}_extra` : base; + return slot === MealSlot.EXTRA ? `${base}_extra` : base; } /** Vrátí dnešní datum, případně fiktivní datum pro účely vývoje a testování. */ @@ -34,7 +34,7 @@ export const getDateForWeekIndex = (index: number) => { } /** Vrátí "prázdná" (implicitní) data pro předaný den. */ -function getEmptyData(date?: Date): ClientData { +export function getEmptyData(date?: Date): ClientData { const usedDate = date || getToday(); return { todayDayIndex: getDayOfWeekIndex(getToday()), @@ -48,9 +48,9 @@ function getEmptyData(date?: Date): ClientData { /** * Vrátí veškerá klientská data pro předaný den, nebo aktuální den, pokud není předán. */ -export async function getData(date?: Date, slot?: string): Promise { +export async function getData(date?: Date, slot?: MealSlot): Promise { const clientData = await getClientData(date, slot); - if (slot !== 'extra') { + if (slot !== MealSlot.EXTRA) { clientData.menus = { SLADOVNICKA: await getRestaurantMenu('SLADOVNICKA', date), // UMOTLIKU: await getRestaurantMenu('UMOTLIKU', date), @@ -59,7 +59,7 @@ export async function getData(date?: Date, slot?: string): Promise { SENKSERIKOVA: await getRestaurantMenu('SENKSERIKOVA', date), } } - if (slot === 'extra') clientData.slot = MealSlot.EXTRA; + if (slot === MealSlot.EXTRA) clientData.slot = MealSlot.EXTRA; return clientData; } @@ -69,7 +69,7 @@ export async function getData(date?: Date, slot?: string): Promise { * @param date datum * @returns databázový klíč */ -function getMenuKey(date: Date) { +export function getMenuKey(date: Date) { const weekNumber = getWeekNumber(date); return `${MENU_PREFIX}_${date.getFullYear()}_${weekNumber}`; } @@ -224,6 +224,7 @@ export async function getRestaurantMenu(restaurant: Restaurant, date?: Date, for for (let i = 0; i < restaurantWeekFood.length && i < weekMenu.length; i++) { weekMenu[i][restaurant]!.food = restaurantWeekFood[i]; weekMenu[i][restaurant]!.lastUpdate = now; + weekMenu[i][restaurant]!.isStale = false; // Detekce uzavření pro každou restauraci switch (restaurant) { @@ -253,22 +254,34 @@ export async function getRestaurantMenu(restaurant: Restaurant, date?: Date, for // Uložení do storage await storage.setData(getMenuKey(usedDate), weekMenu); } catch (e: any) { - console.error(`Selhalo načtení jídel pro podnik ${restaurant}`, e); + if (e instanceof StaleWeekError) { + for (let i = 0; i < e.food.length && i < weekMenu.length; i++) { + weekMenu[i][restaurant]!.food = e.food[i]; + weekMenu[i][restaurant]!.lastUpdate = now; + weekMenu[i][restaurant]!.isStale = true; + } + await storage.setData(getMenuKey(usedDate), weekMenu); + } else { + console.error(`Selhalo načtení jídel pro podnik ${restaurant}`, e); + } } } const result = weekMenu[dayOfWeekIndex][restaurant]!; - result.warnings = generateMenuWarnings(result, now); + result.warnings = generateMenuWarnings(result); return result; } /** * Generuje varování o kvalitě/úplnosti dat menu restaurace. */ -function generateMenuWarnings(menu: RestaurantDayMenu, now: number): string[] { +function generateMenuWarnings(menu: RestaurantDayMenu): string[] { const warnings: string[] = []; if (!menu.food?.length || menu.closed) { return warnings; } + if (menu.isStale) { + warnings.push('Data jsou z minulého týdne'); + } const hasSoup = menu.food.some(f => f.isSoup); if (!hasSoup) { warnings.push('Chybí polévka'); @@ -277,10 +290,6 @@ function generateMenuWarnings(menu: RestaurantDayMenu, now: number): string[] { if (missingPrice) { warnings.push('U některých jídel chybí cena'); } - const STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000; - if (menu.lastUpdate && (now - menu.lastUpdate) > STALE_THRESHOLD_MS) { - warnings.push('Data jsou starší než 24 hodin'); - } return warnings; } @@ -289,7 +298,7 @@ function generateMenuWarnings(menu: RestaurantDayMenu, now: number): string[] { * * @param date datum */ -export async function initIfNeeded(date?: Date, slot?: string) { +export async function initIfNeeded(date?: Date, slot?: MealSlot) { const usedDate = getDataKey(date ?? getToday(), slot); const hasData = await storage.hasData(usedDate); if (!hasData) { @@ -306,7 +315,7 @@ export async function initIfNeeded(date?: Date, slot?: string) { * @param date datum, ke kterému se volba vztahuje * @returns */ -export async function removeChoices(login: string, trusted: boolean, locationKey: LunchChoice, date?: Date, slot?: string) { +export async function removeChoices(login: string, trusted: boolean, locationKey: LunchChoice, date?: Date, slot?: MealSlot) { const selectedDay = getDataKey(date ?? getToday(), slot); let data = await getClientData(date, slot); validateTrusted(data, login, trusted); @@ -333,7 +342,7 @@ export async function removeChoices(login: string, trusted: boolean, locationKey * @param date datum, ke kterému se volba vztahuje * @returns */ -export async function removeChoice(login: string, trusted: boolean, locationKey: LunchChoice, foodIndex: number, date?: Date, slot?: string) { +export async function removeChoice(login: string, trusted: boolean, locationKey: LunchChoice, foodIndex: number, date?: Date, slot?: MealSlot) { const selectedDay = getDataKey(date ?? getToday(), slot); let data = await getClientData(date, slot); validateTrusted(data, login, trusted); @@ -356,7 +365,7 @@ export async function removeChoice(login: string, trusted: boolean, locationKey: * @param date datum, ke kterému se volby vztahují * @param ignoredLocationKey volba, která nebude odstraněna, pokud existuje */ -async function removeChoiceIfPresent(login: string, date?: Date, ignoredLocationKey?: LunchChoice, slot?: string) { +async function removeChoiceIfPresent(login: string, date?: Date, ignoredLocationKey?: LunchChoice, slot?: MealSlot) { const usedDate = date ?? getToday(); let data = await getClientData(usedDate, slot); for (const key of Object.keys(data.choices)) { @@ -408,14 +417,14 @@ function validateTrusted(data: ClientData, login: string, trusted: boolean) { * @param date datum, ke kterému se volba vztahuje * @returns aktuální data */ -export async function addChoice(login: string, trusted: boolean, locationKey: LunchChoice, foodIndex?: number, date?: Date, slot?: string) { +export async function addChoice(login: string, trusted: boolean, locationKey: LunchChoice, foodIndex?: number, date?: Date, slot?: MealSlot) { const usedDate = date ?? getToday(); await initIfNeeded(usedDate, slot); let data = await getClientData(usedDate, slot); validateTrusted(data, login, trusted); await validateFoodIndex(locationKey, foodIndex, date); - if (!slot || slot === 'obed') { + if (!slot || slot === MealSlot.OBED) { // Pokud uživatel měl vybranou PIZZA a mění na něco jiného const hadPizzaChoice = data.choices.PIZZA && login in data.choices.PIZZA; if (hadPizzaChoice && locationKey !== LunchChoice.PIZZA && foodIndex == null) { @@ -498,7 +507,7 @@ async function validateFoodIndex(locationKey: LunchChoice, foodIndex?: number, d * @param note poznámka * @param date datum, ke kterému se volba vztahuje */ -export async function updateNote(login: string, trusted: boolean, note?: string, date?: Date, slot?: string) { +export async function updateNote(login: string, trusted: boolean, note?: string, date?: Date, slot?: MealSlot) { const usedDate = date ?? getToday(); await initIfNeeded(usedDate, slot); let data = await getClientData(usedDate, slot); @@ -522,7 +531,7 @@ export async function updateNote(login: string, trusted: boolean, note?: string, * @param time preferovaný čas odchodu * @param date datum, ke kterému se čas vztahuje */ -export async function updateDepartureTime(login: string, time?: string, date?: Date, slot?: string) { +export async function updateDepartureTime(login: string, time?: string, date?: Date, slot?: MealSlot) { const usedDate = date ?? getToday(); let clientData = await getClientData(usedDate, slot); const found = Object.values(clientData.choices).find(location => login in location); @@ -547,7 +556,7 @@ export async function updateDepartureTime(login: string, time?: string, date?: D * * @param login přihlašovací jméno uživatele */ -export async function updateBuyer(login: string, slot?: string) { +export async function updateBuyer(login: string, slot?: MealSlot) { const usedDate = getToday(); let clientData = await getClientData(usedDate, slot); const userEntry = clientData.choices?.['OBJEDNAVAM']?.[login]; @@ -565,7 +574,7 @@ export async function updateBuyer(login: string, slot?: string) { * @param date datum pro který vrátit data, pokud není vyplněno, je použit dnešní den * @returns data pro klienta */ -export async function getClientData(date?: Date, slot?: string): Promise { +export async function getClientData(date?: Date, slot?: MealSlot): Promise { const targetDate = date ?? getToday(); const dateString = getDataKey(targetDate, slot); const clientData = await storage.getData(dateString) || getEmptyData(date); diff --git a/server/src/storage/index.ts b/server/src/storage/index.ts index 3ac8483..3b611f5 100644 --- a/server/src/storage/index.ts +++ b/server/src/storage/index.ts @@ -3,28 +3,29 @@ import path from 'path'; import { StorageInterface } from "./StorageInterface"; import JsonStorage from "./json"; import RedisStorage from "./redis"; +import MemoryStorage from "./memory"; const ENVIRONMENT = process.env.NODE_ENV ?? 'production'; dotenv.config({ path: path.resolve(__dirname, `../../.env.${ENVIRONMENT}`) }); const JSON_KEY = 'json'; const REDIS_KEY = 'redis'; +const MEMORY_KEY = 'memory'; let storage: StorageInterface; if (!process.env.STORAGE || process.env.STORAGE?.toLowerCase() === JSON_KEY) { storage = new JsonStorage(); } else if (process.env.STORAGE?.toLowerCase() === REDIS_KEY) { storage = new RedisStorage(); +} else if (process.env.STORAGE?.toLowerCase() === MEMORY_KEY) { + storage = new MemoryStorage(); } else { - throw Error("Nepodporovaná hodnota proměnné STORAGE: " + process.env.STORAGE + ", podporované jsou 'json' nebo 'redis'"); + throw Error("Nepodporovaná hodnota proměnné STORAGE: " + process.env.STORAGE + ", podporované jsou 'json', 'redis' nebo 'memory'"); } -(async () => { - if (storage.initialize) { - await storage.initialize(); - } -})(); - +export const storageReady: Promise = storage.initialize + ? storage.initialize() + : Promise.resolve(); export default function getStorage(): StorageInterface { return storage; diff --git a/server/src/storage/memory.ts b/server/src/storage/memory.ts new file mode 100644 index 0000000..b75a41f --- /dev/null +++ b/server/src/storage/memory.ts @@ -0,0 +1,27 @@ +import { StorageInterface } from "./StorageInterface"; + +const store = new Map(); + +/** Vymaže všechna data z in-memory úložiště. Slouží k resetu mezi testy. */ +export function resetMemoryStorage(): void { + store.clear(); +} + +/** + * In-memory implementace úložiště. Používá se výhradně v testovacím prostředí. + */ +export default class MemoryStorage implements StorageInterface { + + hasData(key: string): Promise { + return Promise.resolve(store.has(key)); + } + + getData(key: string): Promise { + return Promise.resolve(store.get(key) as Type | undefined); + } + + setData(key: string, data: Type): Promise { + store.set(key, data); + return Promise.resolve(); + } +} diff --git a/server/src/storage/redis.ts b/server/src/storage/redis.ts index f92720e..bd158c7 100644 --- a/server/src/storage/redis.ts +++ b/server/src/storage/redis.ts @@ -14,7 +14,7 @@ export default class RedisStorage implements StorageInterface { } async initialize() { - client.connect(); + await client.connect(); } async hasData(key: string) { diff --git a/server/src/tests/auth.test.ts b/server/src/tests/auth.test.ts new file mode 100644 index 0000000..39eba1b --- /dev/null +++ b/server/src/tests/auth.test.ts @@ -0,0 +1,83 @@ +import { generateToken, verify, getLogin, getTrusted } from '../auth'; + +const VALID_SECRET = 'test-jwt-secret-ktery-ma-alespon-32-znaku'; +const SHORT_SECRET = 'kratky'; + +beforeEach(() => { + process.env.JWT_SECRET = VALID_SECRET; +}); + +afterEach(() => { + delete process.env.JWT_SECRET; +}); + +describe('generateToken', () => { + test('vrátí token pro platný login', () => { + const token = generateToken('alice'); + expect(typeof token).toBe('string'); + expect(token.length).toBeGreaterThan(0); + }); + + test('vyhodí chybu bez JWT_SECRET', () => { + delete process.env.JWT_SECRET; + expect(() => generateToken('alice')).toThrow('JWT_SECRET'); + }); + + test('vyhodí chybu pro příliš krátký JWT_SECRET', () => { + process.env.JWT_SECRET = SHORT_SECRET; + expect(() => generateToken('alice')).toThrow('32'); + }); + + test('vyhodí chybu pro prázdný login', () => { + expect(() => generateToken('')).toThrow('login'); + }); + + test('vyhodí chybu pro login obsahující jen mezery', () => { + expect(() => generateToken(' ')).toThrow('login'); + }); + + test('vyhodí chybu pro chybějící login', () => { + expect(() => generateToken(undefined)).toThrow('login'); + }); +}); + +describe('verify', () => { + test('vrátí true pro platný token', () => { + const token = generateToken('alice'); + expect(verify(token)).toBe(true); + }); + + test('vrátí false pro podvrženou signaturu', () => { + const token = generateToken('alice'); + const tampered = token.slice(0, -5) + 'XXXXX'; + expect(verify(tampered)).toBe(false); + }); + + test('vrátí false pro token podepsaný jiným secret', () => { + process.env.JWT_SECRET = 'other-secret-min-32-chars-bbbbb!'; + const tokenOther = generateToken('alice'); + process.env.JWT_SECRET = VALID_SECRET; + expect(verify(tokenOther)).toBe(false); + }); +}); + +describe('getLogin / getTrusted', () => { + test('round-trip: getLogin vrátí správný login', () => { + const token = generateToken('bob'); + expect(getLogin(token)).toBe('bob'); + }); + + test('trusted=false je výchozí hodnota', () => { + const token = generateToken('alice'); + expect(getTrusted(token)).toBe(false); + }); + + test('trusted=true je zachováno', () => { + const token = generateToken('alice', true); + expect(getTrusted(token)).toBe(true); + }); + + test('getLogin vyhodí chybu pro chybějící token', () => { + expect(() => getLogin(undefined)).toThrow('token'); + }); +}); diff --git a/server/src/tests/chefie.test.ts b/server/src/tests/chefie.test.ts new file mode 100644 index 0000000..5637a06 --- /dev/null +++ b/server/src/tests/chefie.test.ts @@ -0,0 +1,38 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import axios from 'axios'; +import { downloadSalaty } from '../chefie'; + +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + +const fixturesDir = path.join(__dirname, 'fixtures'); +const loadFixture = (name: string) => fs.readFileSync(path.join(fixturesDir, name), 'utf-8'); + +beforeEach(() => { + jest.resetAllMocks(); + // První volání = stránka se seznamem salátů, následující volání = jednotlivé stránky salátů + mockedAxios.get = jest.fn() + .mockResolvedValueOnce({ data: loadFixture('chefie-salaty.html') }) + .mockResolvedValueOnce({ data: loadFixture('chefie-salat-caesar.html') }) + .mockResolvedValueOnce({ data: loadFixture('chefie-salat-recky.html') }); +}); + +test('downloadSalaty vrátí seznam salátů', async () => { + const salaty = await downloadSalaty(false); + expect(salaty).toHaveLength(2); +}); + +test('saláty mají name a ingredients', async () => { + const salaty = await downloadSalaty(false); + expect(salaty[0].name).toBe('Caesar salát'); + expect(salaty[0].ingredients).toContain('Kuřecí maso'); +}); + +test('cena salátu zahrnuje pevný příplatek 13 Kč za obal', async () => { + const salaty = await downloadSalaty(false); + // Caesar sticker price = 129, box = 13 + expect(salaty[0].price).toBe(129 + 13); + // Řecký sticker price = 119, box = 13 + expect(salaty[1].price).toBe(119 + 13); +}); diff --git a/server/src/tests/fixtures/chefie-salat-caesar.html b/server/src/tests/fixtures/chefie-salat-caesar.html new file mode 100644 index 0000000..c2c7973 --- /dev/null +++ b/server/src/tests/fixtures/chefie-salat-caesar.html @@ -0,0 +1,16 @@ + + + +
+

Caesar salát

+
+
    +
  • Ledový salát
  • +
  • Kuřecí maso
  • +
  • Parmazán
  • +
+
+ 129 Kč +
+ + diff --git a/server/src/tests/fixtures/chefie-salat-recky.html b/server/src/tests/fixtures/chefie-salat-recky.html new file mode 100644 index 0000000..86cf988 --- /dev/null +++ b/server/src/tests/fixtures/chefie-salat-recky.html @@ -0,0 +1,16 @@ + + + +
+

Řecký salát

+
+
    +
  • Rajčata
  • +
  • Okurka
  • +
  • Feta sýr
  • +
+
+ 119 Kč +
+ + diff --git a/server/src/tests/fixtures/chefie-salaty.html b/server/src/tests/fixtures/chefie-salaty.html new file mode 100644 index 0000000..39fb481 --- /dev/null +++ b/server/src/tests/fixtures/chefie-salaty.html @@ -0,0 +1,13 @@ + + + + + + diff --git a/server/src/tests/fixtures/senkserikova.html b/server/src/tests/fixtures/senkserikova.html new file mode 100644 index 0000000..6848aa1 --- /dev/null +++ b/server/src/tests/fixtures/senkserikova.html @@ -0,0 +1,33 @@ + + + +
+ +
+
+ +
+ + diff --git a/server/src/tests/fixtures/sladovnicka.html b/server/src/tests/fixtures/sladovnicka.html new file mode 100644 index 0000000..86b8745 --- /dev/null +++ b/server/src/tests/fixtures/sladovnicka.html @@ -0,0 +1,55 @@ + + + +
    + + + + + +
+
    +
    +
    + + + + +
    250mlPolévka dne 1, 935 Kč
    150gSvíčková na smetaně s knedlíkem 1, 3, 7149 Kč
    120gKuřecí řízek s bramborami 1139 Kč
    +
    +
    +
    +
    + + + +
    250mlČesnečka 135 Kč
    150gVepřový guláš s houskovým knedlíkem 1, 3145 Kč
    +
    +
    +
    +
    + + + +
    250mlHovězí vývar s nudlemi 135 Kč
    150gSmažený sýr s bramborovým salátem 1, 3, 7135 Kč
    +
    +
    +
    +
    + + + +
    250mlRajská polévka 135 Kč
    150gRizoto s kuřecím masem 1139 Kč
    +
    +
    +
    +
    + + + +
    250mlDršťková polévka 135 Kč
    150gSegedínský guláš s knedlíkem 1, 3145 Kč
    +
    +
    +
+ + diff --git a/server/src/tests/fixtures/techtower.html b/server/src/tests/fixtures/techtower.html new file mode 100644 index 0000000..74bf729 --- /dev/null +++ b/server/src/tests/fixtures/techtower.html @@ -0,0 +1,29 @@ + + + +
+
+

+ Obědy 12.5.-16.5.2025 +

+
+ +

Pondělí

+

• Polévka dne 1

+

• Svíčková na smetaně s knedlíkem 1, 3, 7 149 Kč

+

• Smažený sýr s bramborami 1, 3 139 Kč

+

Úterý

+

• Česnečka 1

+

• Vepřový guláš s houskovým knedlíkem 1, 3 145 Kč

+

Středa

+

• Hovězí vývar s nudlemi 1

+

• Kuřecí řízek s bramborami 1 139 Kč

+

Čtvrtek

+

• Dršťková polévka 1

+

• Segedínský guláš s knedlíkem 1, 3 145 Kč

+

Pátek

+

• Rajská polévka s rýží 1

+

• Rizoto s kuřecím masem a zeleninou 1 139 Kč

+
+ + diff --git a/server/src/tests/generateQr.test.ts b/server/src/tests/generateQr.test.ts new file mode 100644 index 0000000..5648907 --- /dev/null +++ b/server/src/tests/generateQr.test.ts @@ -0,0 +1,47 @@ +import axios from 'axios'; +import { generateQr, getQr } from '../qr'; +import { resetMemoryStorage } from '../storage/memory'; + +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + +const FAKE_IMAGE = Buffer.from('fake-png-data'); + +beforeEach(() => { + resetMemoryStorage(); + jest.resetAllMocks(); + mockedAxios.get = jest.fn().mockResolvedValue({ data: FAKE_IMAGE }); +}); + +test('generateQr zavolá Paylibo API se správnými parametry', async () => { + await generateQr('jannovak', '19-2000145399/0800', 'Jan Novák', 149, 'Pizza Margherita', 'test-uuid-1'); + expect(mockedAxios.get).toHaveBeenCalledTimes(1); + const [url, config] = (mockedAxios.get as jest.Mock).mock.calls[0]; + expect(url).toContain('paylibo.com'); + expect(config.params.amount).toBe(149); + expect(config.params.iban).toBeDefined(); +}); + +test('generateQr uloží base64 obrázek do storage', async () => { + await generateQr('jannovak', '19-2000145399/0800', 'Jan Novák', 149, 'Pizza', 'test-uuid-2'); + const img = await getQr('jannovak', 'test-uuid-2'); + expect(Buffer.isBuffer(img)).toBe(true); + expect(img).toEqual(FAKE_IMAGE); +}); + +test('generateQr ořeže zprávu delší než 60 znaků', async () => { + const dlouhaZprava = 'Pizza ' + 'x'.repeat(60); + await generateQr('jannovak', '19-2000145399/0800', 'Jan Novák', 149, dlouhaZprava, 'test-uuid-3'); + const [, config] = (mockedAxios.get as jest.Mock).mock.calls[0]; + expect(config.params.message.length).toBeLessThanOrEqual(60); +}); + +test('generateQr odstraní hvězdičku ze zprávy', async () => { + await generateQr('jannovak', '19-2000145399/0800', 'Jan Novák', 149, 'Pizza *Margherita*', 'test-uuid-4'); + const [, config] = (mockedAxios.get as jest.Mock).mock.calls[0]; + expect(config.params.message).not.toContain('*'); +}); + +test('getQr hodí chybu pro neexistující ID', async () => { + await expect(getQr('jannovak', 'neexistuje')).rejects.toThrow('nebyl nalezen'); +}); diff --git a/server/src/tests/helpers/setupEnv.ts b/server/src/tests/helpers/setupEnv.ts new file mode 100644 index 0000000..174392a --- /dev/null +++ b/server/src/tests/helpers/setupEnv.ts @@ -0,0 +1,4 @@ +process.env.NODE_ENV = 'test'; +process.env.JWT_SECRET = 'test-secret-min-32-chars-aaaaaaa!'; +process.env.MOCK_DATA = 'true'; +process.env.STORAGE = 'json'; diff --git a/server/src/tests/pizza.test.ts b/server/src/tests/pizza.test.ts new file mode 100644 index 0000000..34f4519 --- /dev/null +++ b/server/src/tests/pizza.test.ts @@ -0,0 +1,235 @@ +import { resetMemoryStorage } from '../storage/memory'; +import getStorage from '../storage'; +import { formatDate } from '../utils'; +import { getToday } from '../service'; +import { + createPizzaDay, + deletePizzaDay, + addPizzaOrder, + removePizzaOrder, + removeAllUserPizzas, + updatePizzaFee, + lockPizzaDay, + unlockPizzaDay, + finishPizzaOrder, + finishPizzaDelivery, +} from '../pizza'; +import { ClientData, PizzaDayState } from '../../../types/gen/types.gen'; + +jest.mock('../notifikace', () => ({ + callNotifikace: jest.fn().mockResolvedValue([]), +})); +jest.mock('../qr', () => ({ + generateQr: jest.fn().mockResolvedValue(undefined), +})); +jest.mock('../chefie', () => ({ + downloadPizzy: jest.fn().mockResolvedValue([ + { id: 1, name: 'Margherita', variants: [{ varId: 10, size: 'střední', price: 150 }] }, + ]), + downloadSalaty: jest.fn().mockResolvedValue([]), +})); + +const CREATOR = 'kreator'; +const USER = 'uzivatel'; + +const PIZZA: any = { id: 1, name: 'Margherita', ingredients: [], variants: [], sizes: [] }; +const SIZE_M = { varId: 1, size: '30cm', price: 179, boxPrice: 13, pizzaPrice: 166 }; +const SIZE_L = { varId: 2, size: '35cm', price: 219, boxPrice: 15, pizzaPrice: 204 }; +const SIZE: any = { varId: 10, size: 'střední', price: 150 }; + +async function seedPizzaDay(state: PizzaDayState = PizzaDayState.CREATED): Promise { + const today = formatDate(getToday()); + const storage = getStorage(); + const data: ClientData = { + todayDayIndex: 0, + date: today, + isWeekend: false, + dayIndex: 0, + choices: {}, + pizzaDay: { + state, + creator: CREATOR, + orders: [], + }, + pizzaList: [], + salatList: [], + }; + await storage.setData(today, data); +} + +beforeEach(() => { + resetMemoryStorage(); +}); + +describe('createPizzaDay', () => { + test('vytvoří pizza day ve stavu CREATED', async () => { + const data = await createPizzaDay(CREATOR); + expect(data.pizzaDay?.state).toBe(PizzaDayState.CREATED); + expect(data.pizzaDay?.creator).toBe(CREATOR); + }); + + test('vyhodí chybu, pokud pizza day již existuje', async () => { + await createPizzaDay(CREATOR); + await expect(createPizzaDay(CREATOR)).rejects.toThrow('existuje'); + }); +}); + +describe('deletePizzaDay', () => { + test('smaže pizza day tvůrcem', async () => { + await createPizzaDay(CREATOR); + const data = await deletePizzaDay(CREATOR); + expect(data.pizzaDay).toBeUndefined(); + }); + + test('vyhodí chybu pro jiného uživatele', async () => { + await createPizzaDay(CREATOR); + await expect(deletePizzaDay(USER)).rejects.toThrow(); + }); +}); + +describe('addPizzaOrder', () => { + test('přidá objednávku pizzy', async () => { + await seedPizzaDay(); + const data = await addPizzaOrder(USER, PIZZA, SIZE); + const order = data.pizzaDay?.orders?.find(o => o.customer === USER); + expect(order?.pizzaList?.length).toBe(1); + expect(order?.totalPrice).toBe(SIZE.price); + }); + + test('přičte cenu další pizzy ke stejné objednávce', async () => { + await seedPizzaDay(); + await addPizzaOrder(USER, PIZZA, SIZE_M); + const data = await addPizzaOrder(USER, { ...PIZZA, name: 'Quattro' }, SIZE_L); + const order = data.pizzaDay?.orders?.find(o => o.customer === USER); + expect(order?.totalPrice).toBe(SIZE_M.price + SIZE_L.price); + expect(order?.pizzaList).toHaveLength(2); + }); + + test('vyhodí chybu bez aktivního pizza day', async () => { + await expect(addPizzaOrder(USER, PIZZA, SIZE)).rejects.toThrow('neexistuje'); + }); + + test('vyhodí chybu pro pizza day ve stavu LOCKED', async () => { + await seedPizzaDay(PizzaDayState.LOCKED); + await expect(addPizzaOrder(USER, PIZZA, SIZE_M)).rejects.toThrow(PizzaDayState.CREATED); + }); +}); + +describe('removePizzaOrder', () => { + test('odečte cenu a odstraní položku z objednávky', async () => { + await seedPizzaDay(); + await addPizzaOrder(USER, PIZZA, SIZE_M); + await addPizzaOrder(USER, { ...PIZZA, name: 'Diavola' }, SIZE_L); + const variant = { varId: SIZE_M.varId, name: PIZZA.name, size: SIZE_M.size, price: SIZE_M.price }; + const data = await removePizzaOrder(USER, variant); + const order = data.pizzaDay?.orders?.find(o => o.customer === USER); + expect(order?.totalPrice).toBe(SIZE_L.price); + expect(order?.pizzaList).toHaveLength(1); + }); + + test('odstraní celou objednávku, pokud je prázdná', async () => { + await seedPizzaDay(); + await addPizzaOrder(USER, PIZZA, SIZE_M); + const variant = { varId: SIZE_M.varId, name: PIZZA.name, size: SIZE_M.size, price: SIZE_M.price }; + const data = await removePizzaOrder(USER, variant); + const order = data.pizzaDay?.orders?.find(o => o.customer === USER); + expect(order).toBeUndefined(); + }); +}); + +describe('removeAllUserPizzas', () => { + test('odstraní objednávku uživatele', async () => { + await seedPizzaDay(); + await addPizzaOrder(USER, PIZZA, SIZE); + const data = await removeAllUserPizzas(USER); + const order = data.pizzaDay?.orders?.find(o => o.customer === USER); + expect(order).toBeUndefined(); + }); + + test('je no-op bez pizza day', async () => { + const data = await removeAllUserPizzas(USER); + expect(data.pizzaDay).toBeUndefined(); + }); +}); + +describe('updatePizzaFee', () => { + test('přidá příplatek a přepočítá celkovou cenu', async () => { + await seedPizzaDay(); + await addPizzaOrder(USER, PIZZA, SIZE_M); + const data = await updatePizzaFee(CREATOR, USER, 'Balné', 20); + const order = data.pizzaDay?.orders?.find(o => o.customer === USER); + expect(order?.fee).toEqual({ text: 'Balné', price: 20 }); + expect(order?.totalPrice).toBe(SIZE_M.price + 20); + }); + + test('s cenou undefined odstraní příplatek', async () => { + await seedPizzaDay(); + await addPizzaOrder(USER, PIZZA, SIZE_M); + await updatePizzaFee(CREATOR, USER, 'Balné', 20); + const data = await updatePizzaFee(CREATOR, USER, undefined, undefined); + const order = data.pizzaDay?.orders?.find(o => o.customer === USER); + expect(order?.fee).toBeUndefined(); + expect(order?.totalPrice).toBe(SIZE_M.price); + }); + + test('vyhodí chybu, pokud volá jiný uživatel než tvůrce', async () => { + await seedPizzaDay(); + await addPizzaOrder(USER, PIZZA, SIZE_M); + await expect(updatePizzaFee(USER, USER, 'Balné', 20)).rejects.toThrow('zakladatel'); + }); +}); + +describe('lockPizzaDay / unlockPizzaDay', () => { + test('tvůrce může zamknout pizza day', async () => { + await seedPizzaDay(); + const data = await lockPizzaDay(CREATOR); + expect(data.pizzaDay?.state).toBe(PizzaDayState.LOCKED); + }); + + test('jiný uživatel nemůže zamknout pizza day', async () => { + await seedPizzaDay(); + await expect(lockPizzaDay(USER)).rejects.toThrow(USER); + }); + + test('zamčený pizza day lze odemknout', async () => { + await seedPizzaDay(); + await lockPizzaDay(CREATOR); + const data = await unlockPizzaDay(CREATOR); + expect(data.pizzaDay?.state).toBe(PizzaDayState.CREATED); + }); + + test('nelze odemknout nezamčený pizza day', async () => { + await seedPizzaDay(); + await expect(unlockPizzaDay(CREATOR)).rejects.toThrow(PizzaDayState.LOCKED); + }); +}); + +describe('finishPizzaOrder', () => { + test('přesune pizza day do stavu ORDERED', async () => { + await seedPizzaDay(); + await lockPizzaDay(CREATOR); + const data = await finishPizzaOrder(CREATOR); + expect(data.pizzaDay?.state).toBe(PizzaDayState.ORDERED); + }); + + test('vyhodí chybu v nesprávném stavu (CREATED)', async () => { + await seedPizzaDay(); + await expect(finishPizzaOrder(CREATOR)).rejects.toThrow(PizzaDayState.LOCKED); + }); +}); + +describe('finishPizzaDelivery', () => { + test('přesune pizza day do stavu DELIVERED', async () => { + await seedPizzaDay(); + await lockPizzaDay(CREATOR); + await finishPizzaOrder(CREATOR); + const data = await finishPizzaDelivery(CREATOR); + expect(data.pizzaDay?.state).toBe(PizzaDayState.DELIVERED); + }); + + test('vyhodí chybu v nesprávném stavu (LOCKED)', async () => { + await seedPizzaDay(); + await lockPizzaDay(CREATOR); + await expect(finishPizzaDelivery(CREATOR)).rejects.toThrow(PizzaDayState.ORDERED); + }); +}); diff --git a/server/src/tests/qr.test.ts b/server/src/tests/qr.test.ts new file mode 100644 index 0000000..34d50a9 --- /dev/null +++ b/server/src/tests/qr.test.ts @@ -0,0 +1,36 @@ +import { convertBbanToIban } from '../qr'; + +test('konverze BBAN s prefixem na IBAN', () => { + // Číslo účtu 19-2000145399/0800 (Česká spořitelna) → CZ6508000000192000145399 + const iban = convertBbanToIban('19-2000145399/0800'); + expect(iban).toBe('CZ6508000000192000145399'); + expect(iban).toHaveLength(24); +}); + +test('konverze BBAN bez prefixu na IBAN', () => { + // Číslo účtu 2000145399/0800 (bez prefixu) → prefix se doplní jako 000000 + const iban = convertBbanToIban('2000145399/0800'); + expect(iban).toBe('CZ7908000000002000145399'); + expect(iban).toHaveLength(24); +}); + +test('konverze BBAN s krátkým číslem účtu – zero-padding', () => { + // Krátké číslo účtu 123456/0100 → prefix 000000, account 0000123456 + const iban = convertBbanToIban('123456/0100'); + expect(iban).toHaveLength(24); + // bankCode(4) + prefix(6) + account(10) = 20 číslic za CZ+checkdigits + expect(iban).toMatch(/^CZ\d{2}01000000000000123456$/); +}); + +test('kontrolní číslice jsou platné (mod 97)', () => { + const iban = convertBbanToIban('19-2000145399/0800'); + // Přesuneme první 4 znaky na konec, nahradíme písmena čísly a mod 97 musí dát 1 + const rearranged = iban.slice(4) + iban.slice(0, 4); + const numeric = rearranged.replace(/[A-Z]/g, (c) => (c.charCodeAt(0) - 55).toString()); + expect(BigInt(numeric) % BigInt(97)).toBe(BigInt(1)); +}); + +test('výsledek vždy začíná CZ', () => { + expect(convertBbanToIban('100-2000145399/0300')).toMatch(/^CZ/); + expect(convertBbanToIban('2000145399/0100')).toMatch(/^CZ/); +}); diff --git a/server/src/tests/qrRoutes.test.ts b/server/src/tests/qrRoutes.test.ts new file mode 100644 index 0000000..a558111 --- /dev/null +++ b/server/src/tests/qrRoutes.test.ts @@ -0,0 +1,103 @@ +import express from 'express'; +import request from 'supertest'; +import bodyParser from 'body-parser'; +import axios from 'axios'; +import { generateToken } from '../auth'; +import { resetMemoryStorage } from '../storage/memory'; +import qrRouter from '../routes/qrRoutes'; + +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + +function buildApp() { + const app = express(); + app.use(bodyParser.json()); + app.use('/api/qr', qrRouter); + app.use((err: any, _req: any, res: any, _next: any) => { + res.status(400).json({ error: err.message }); + }); + return app; +} + +const TOKEN = `Bearer ${generateToken('kreator')}`; + +beforeEach(() => { + resetMemoryStorage(); + mockedAxios.get = jest.fn().mockResolvedValue({ data: Buffer.from('fake-png') }); +}); + +const VALID_BODY = { + recipients: [ + { login: 'uzivatel1', purpose: 'Pizza Margherita', amount: 149 }, + { login: 'uzivatel2', purpose: 'Pizza Diavola', amount: 179 }, + ], + bankAccount: '19-2000145399/0800', + bankAccountHolder: 'Jan Novák', +}; + +test('POST /generate vrátí 200 s počtem vygenerovaných QR kódů', async () => { + const res = await request(buildApp()) + .post('/api/qr/generate') + .set('Authorization', TOKEN) + .send(VALID_BODY); + expect(res.status).toBe(200); + expect(res.body.count).toBe(2); +}); + +test('POST /generate vrátí 400 pro prázdné recipients', async () => { + const res = await request(buildApp()) + .post('/api/qr/generate') + .set('Authorization', TOKEN) + .send({ ...VALID_BODY, recipients: [] }); + expect(res.status).toBe(400); + expect(res.body.error).toContain('příjemců'); +}); + +test('POST /generate vrátí 400 pro chybějící bankAccount', async () => { + const { bankAccount: _, ...body } = VALID_BODY; + const res = await request(buildApp()) + .post('/api/qr/generate') + .set('Authorization', TOKEN) + .send(body); + expect(res.status).toBe(400); + expect(res.body.error).toContain('účtu'); +}); + +test('POST /generate vrátí 400 pro zápornou částku', async () => { + const body = { + ...VALID_BODY, + recipients: [{ login: 'uzivatel1', purpose: 'Pizza', amount: -1 }], + }; + const res = await request(buildApp()) + .post('/api/qr/generate') + .set('Authorization', TOKEN) + .send(body); + expect(res.status).toBe(400); + expect(res.body.error).toContain('částku'); +}); + +test('POST /generate vrátí 400 pro částku s více než 2 desetinnými místy', async () => { + const body = { + ...VALID_BODY, + recipients: [{ login: 'uzivatel1', purpose: 'Pizza', amount: 149.999 }], + }; + const res = await request(buildApp()) + .post('/api/qr/generate') + .set('Authorization', TOKEN) + .send(body); + expect(res.status).toBe(400); + expect(res.body.error).toContain('desetinná'); +}); + +test('POST /generate vrátí 400 pro příjemce bez login', async () => { + const body = { + ...VALID_BODY, + recipients: [{ purpose: 'Pizza', amount: 149 }], + }; + const res = await request(buildApp()) + .post('/api/qr/generate') + .set('Authorization', TOKEN) + .send(body); + expect(res.status).toBe(400); + expect(res.body.error).toContain('login'); +}); diff --git a/server/src/tests/restaurants-helpers.test.ts b/server/src/tests/restaurants-helpers.test.ts new file mode 100644 index 0000000..85546ff --- /dev/null +++ b/server/src/tests/restaurants-helpers.test.ts @@ -0,0 +1,73 @@ +import { parseAllergens, isTextSoupName, sanitizeText, capitalize } from '../restaurants'; + +// parseAllergens +test('parseAllergens rozpozná alergeny na konci názvu', () => { + const result = parseAllergens('Svíčková na smetaně 1, 3, 7'); + expect(result.cleanName).toBe('Svíčková na smetaně'); + expect(result.allergens).toEqual([1, 3, 7]); +}); + +test('parseAllergens vrátí prázdné pole alergenů, pokud žádné nejsou', () => { + const result = parseAllergens('Svíčková na smetaně'); + expect(result.cleanName).toBe('Svíčková na smetaně'); + expect(result.allergens).toEqual([]); +}); + +test('parseAllergens zpracuje jednočíselný alergen', () => { + const result = parseAllergens('Polévka dne 1'); + expect(result.cleanName).toBe('Polévka dne'); + expect(result.allergens).toEqual([1]); +}); + +test('parseAllergens neodstraní čísla uvnitř názvu', () => { + const result = parseAllergens('Pizza č. 4 Quattro formaggi 1, 7'); + expect(result.allergens).toEqual([1, 7]); + expect(result.cleanName).toContain('4'); +}); + +// isTextSoupName +test('isTextSoupName vrátí true pro "polévka"', () => { + expect(isTextSoupName('Polévka dne')).toBe(true); +}); + +test('isTextSoupName vrátí true pro "česnečka"', () => { + expect(isTextSoupName('Česnečka se sýrem')).toBe(true); +}); + +test('isTextSoupName vrátí true pro "vývar"', () => { + expect(isTextSoupName('Hovězí vývar s nudlemi')).toBe(true); +}); + +test('isTextSoupName vrátí false pro hlavní jídlo', () => { + expect(isTextSoupName('Svíčková na smetaně s knedlíkem')).toBe(false); +}); + +test('isTextSoupName není case-sensitive', () => { + expect(isTextSoupName('POLÉVKA DNE')).toBe(true); +}); + +// sanitizeText +test('sanitizeText odstraní tabulátor (nenahradí mezerou)', () => { + expect(sanitizeText('\ttext')).toBe('text'); +}); + +test('sanitizeText opraví mezery kolem čárky', () => { + expect(sanitizeText('jídlo , příloha')).toBe('jídlo, příloha'); +}); + +test('sanitizeText ořeže mezery ze začátku a konce', () => { + expect(sanitizeText(' text ')).toBe('text'); +}); + +// capitalize +test('capitalize převede první písmeno na velké', () => { + expect(capitalize('pondělí')).toBe('Pondělí'); +}); + +test('capitalize nezmění zbytek řetězce', () => { + expect(capitalize('pÁTEK')).toBe('PÁTEK'); +}); + +test('capitalize vrátí prázdný řetězec pro prázdný vstup', () => { + expect(capitalize('')).toBe(''); +}); diff --git a/server/src/tests/restaurants.test.ts b/server/src/tests/restaurants.test.ts new file mode 100644 index 0000000..511941c --- /dev/null +++ b/server/src/tests/restaurants.test.ts @@ -0,0 +1,106 @@ +import { isTextSoupName, capitalize, sanitizeText, parseAllergens } from '../restaurants'; + +describe('isTextSoupName', () => { + test('rozpozná "polévka"', () => { + expect(isTextSoupName('Polévka dne')).toBe(true); + }); + + test('rozpozná "česnečka"', () => { + expect(isTextSoupName('Česnečka s krutony')).toBe(true); + }); + + test('rozpozná "vývar"', () => { + expect(isTextSoupName('Hovězí vývar s nudlemi')).toBe(true); + }); + + test('rozpozná "slepičí s " (parciální shoda pro slepičí vývar)', () => { + expect(isTextSoupName('Slepičí s nudlemi')).toBe(true); + }); + + test('neklasifikuje hlavní jídlo jako polévku', () => { + expect(isTextSoupName('Svíčková na smetaně s knedlíky')).toBe(false); + }); + + test('neklasifikuje prázdný řetězec', () => { + expect(isTextSoupName('')).toBe(false); + }); + + test('není case-sensitive', () => { + expect(isTextSoupName('POLÉVKA DNEŠKA')).toBe(true); + }); +}); + +describe('capitalize', () => { + test('zformátuje první písmeno na velké', () => { + expect(capitalize('svíčková')).toBe('Svíčková'); + }); + + test('nechá velká písmena beze změny', () => { + expect(capitalize('ABC')).toBe('ABC'); + }); + + test('prázdný řetězec zůstane prázdný', () => { + expect(capitalize('')).toBe(''); + }); + + test('jednoznakový řetězec', () => { + expect(capitalize('a')).toBe('A'); + }); +}); + +describe('sanitizeText', () => { + test('odstraní tabulátor (první výskyt)', () => { + // replace('\t', '') odstraní tab bez přidání mezery + expect(sanitizeText('\tKnedlíky')).toBe('Knedlíky'); + }); + + test('nahradí první " , " za ", "', () => { + // replace(' , ', ', ') nahrazuje pouze první výskyt + expect(sanitizeText('Knedlíky , zelí')).toBe('Knedlíky, zelí'); + }); + + test('ořízne okrajové mezery', () => { + expect(sanitizeText(' Jídlo ')).toBe('Jídlo'); + }); + + test('kombinace: tab + mezera okolo čárky', () => { + expect(sanitizeText('\tKnedlíky , zelí ')).toBe('Knedlíky, zelí'); + }); +}); + +describe('parseAllergens', () => { + test('extrahuje alergeny na konci řetězce', () => { + const result = parseAllergens('Svíčková 1,3,7'); + expect(result.cleanName).toBe('Svíčková'); + expect(result.allergens).toEqual([1, 3, 7]); + }); + + test('toleruje mezery okolo čárek v alergenech', () => { + const result = parseAllergens('Řízek 1, 3, 7'); + expect(result.allergens).toEqual([1, 3, 7]); + }); + + test('vrátí prázdná pole pro jídlo bez alergenů', () => { + const result = parseAllergens('Ovocný salát'); + expect(result.cleanName).toBe('Ovocný salát'); + expect(result.allergens).toEqual([]); + }); + + test('nesplete se s číslem uprostřed názvu', () => { + const result = parseAllergens('Jídlo č. 5 bez alergenů'); + expect(result.cleanName).toBe('Jídlo č. 5 bez alergenů'); + expect(result.allergens).toEqual([]); + }); + + test('single alergen', () => { + const result = parseAllergens('Houby 7'); + expect(result.cleanName).toBe('Houby'); + expect(result.allergens).toEqual([7]); + }); + + test('prázdný řetězec vrátí prázdné výsledky', () => { + const result = parseAllergens(''); + expect(result.cleanName).toBe(''); + expect(result.allergens).toEqual([]); + }); +}); diff --git a/server/src/tests/scrapers.test.ts b/server/src/tests/scrapers.test.ts new file mode 100644 index 0000000..06c0669 --- /dev/null +++ b/server/src/tests/scrapers.test.ts @@ -0,0 +1,117 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import axios from 'axios'; +import { getMenuSladovnicka, getMenuTechTower, getMenuSenkSerikova, StaleWeekError } from '../restaurants'; + +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + +const fixturesDir = path.join(__dirname, 'fixtures'); +const loadFixture = (name: string) => fs.readFileSync(path.join(fixturesDir, name), 'utf-8'); + +// Pondělí 12.5.2025 +const MONDAY = new Date('2025-05-12'); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('Sladovnicka parser', () => { + beforeEach(() => { + mockedAxios.get = jest.fn().mockResolvedValue({ data: loadFixture('sladovnicka.html') }); + }); + + test('vrátí pole o délce 5 (jeden záznam na každý pracovní den)', async () => { + const menu = await getMenuSladovnicka(MONDAY); + expect(menu).toHaveLength(5); + }); + + test('pondělní menu obsahuje aspoň jedno jídlo', async () => { + const menu = await getMenuSladovnicka(MONDAY); + expect(menu[0].length).toBeGreaterThan(0); + }); + + test('první položka pondělního dne je polévka (isSoup=true)', async () => { + const menu = await getMenuSladovnicka(MONDAY); + expect(menu[0][0].isSoup).toBe(true); + }); + + test('jídla mají name, price a amount', async () => { + const menu = await getMenuSladovnicka(MONDAY); + const jidlo = menu[0][1]; + expect(jidlo.name).toBeTruthy(); + expect(jidlo.price).toBeTruthy(); + expect(jidlo.amount).toBeTruthy(); + }); + + test('alergeny jsou naparsovány jako čísla', async () => { + const menu = await getMenuSladovnicka(MONDAY); + const polievka = menu[0][0]; + expect(Array.isArray(polievka.allergens)).toBe(true); + expect(polievka.allergens!.length).toBeGreaterThan(0); + expect(typeof polievka.allergens![0]).toBe('number'); + }); +}); + +describe('TechTower parser', () => { + beforeEach(() => { + mockedAxios.get = jest.fn().mockResolvedValue({ data: loadFixture('techtower.html') }); + }); + + test('vrátí pole o délce 5', async () => { + const menu = await getMenuTechTower(MONDAY); + expect(menu).toHaveLength(5); + }); + + test('pondělní menu obsahuje polévku a hlavní jídla', async () => { + const menu = await getMenuTechTower(MONDAY); + expect(menu[0].some(f => f.isSoup)).toBe(true); + expect(menu[0].some(f => !f.isSoup)).toBe(true); + }); + + test('TechTower hodí StaleWeekError, pokud datum v hlavičce neodpovídá', async () => { + // Fixture obsahuje "12.5.-16.5.2025" – jiný týden = stale + const jinaStreda = new Date('2025-04-14'); + await expect(getMenuTechTower(jinaStreda)).rejects.toBeInstanceOf(StaleWeekError); + }); + + test('StaleWeekError obsahuje naparsovaná data', async () => { + const jinaStreda = new Date('2025-04-14'); + try { + await getMenuTechTower(jinaStreda); + } catch (e) { + expect(e).toBeInstanceOf(StaleWeekError); + const err = e as StaleWeekError; + expect(err.food).toHaveLength(5); + } + }); +}); + +describe('SenkSerikova parser', () => { + beforeEach(() => { + // SenkSerikova parsuje arraybuffer – musíme vrátit Buffer, ne string + mockedAxios.get = jest.fn().mockResolvedValue({ + data: Buffer.from(loadFixture('senkserikova.html')), + headers: {} + }); + }); + + test('parser provede HTTP request a vrátí pole', async () => { + const menu = await getMenuSenkSerikova(MONDAY); + expect(mockedAxios.get).toHaveBeenCalledTimes(1); + expect(Array.isArray(menu)).toBe(true); + }); + + test('výsledné dny s obsahem mají správnou strukturu (name, price, isSoup)', async () => { + const menu = await getMenuSenkSerikova(MONDAY); + // Protože MONDAY je v minulosti, parser vrátí placeholdery pro všechny pracovní + // dny a .menicka elementy přidá za ně – hledáme aspoň jeden den s reálnými daty + const denSJidlem = menu.find(den => + den.length > 0 && den[0].name !== 'Pro tento den není uveřejněna nabídka jídel' + ); + if (denSJidlem) { + expect(typeof denSJidlem[0].name).toBe('string'); + expect(typeof denSJidlem[0].isSoup).toBe('boolean'); + } + }); +}); diff --git a/server/src/tests/service.slot.test.ts b/server/src/tests/service.slot.test.ts new file mode 100644 index 0000000..e68430b --- /dev/null +++ b/server/src/tests/service.slot.test.ts @@ -0,0 +1,59 @@ +const mockStorageData = new Map(); +jest.mock('../storage', () => ({ + __esModule: true, + default: () => ({ + hasData: async (key: string) => mockStorageData.has(key), + getData: async (key: string) => mockStorageData.get(key) as T, + setData: async (key: string, val: T) => void mockStorageData.set(key, val), + }), + storageReady: Promise.resolve(), +})); + +import { addChoice, getData } from '../service'; +import { LunchChoice, MealSlot } from '../../../types/gen/types.gen'; + +const TODAY = new Date('2025-01-10'); +const TODAY_STR = '2025-01-10'; +const TODAY_EXTRA_STR = '2025-01-10_extra'; + +describe('MealSlot storage isolation', () => { + beforeEach(() => { + mockStorageData.clear(); + jest.useFakeTimers(); + jest.setSystemTime(TODAY); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + test('addChoice slot=extra writes only to _extra key, not to obed key', async () => { + await addChoice('user1', false, LunchChoice.OBJEDNAVAM, undefined, TODAY, MealSlot.EXTRA); + expect(mockStorageData.has(TODAY_EXTRA_STR)).toBe(true); + expect(mockStorageData.has(TODAY_STR)).toBe(false); + const extraData = mockStorageData.get(TODAY_EXTRA_STR); + expect(extraData.choices.OBJEDNAVAM?.['user1']).toBeDefined(); + }); + + test('getData slot=extra returns slot===MealSlot.EXTRA and no menus', async () => { + await addChoice('user1', false, LunchChoice.OBJEDNAVAM, undefined, TODAY, MealSlot.EXTRA); + const result = await getData(TODAY, MealSlot.EXTRA); + expect(result.slot).toBe(MealSlot.EXTRA); + expect(result.menus).toBeUndefined(); + }); + + test('addChoice slot=extra does not modify obed data even when obed has PIZZA choice', async () => { + mockStorageData.set(TODAY_STR, { + choices: { PIZZA: { user1: { selectedFoods: [0], trusted: false } } }, + todayDayIndex: 4, + date: '10. 1. 2025', + isWeekend: false, + dayIndex: 4, + }); + + await addChoice('user1', false, LunchChoice.OBJEDNAVAM, undefined, TODAY, MealSlot.EXTRA); + + const obed = mockStorageData.get(TODAY_STR); + expect(obed.choices.PIZZA?.['user1']).toBeDefined(); + }); +}); diff --git a/server/src/tests/service.test.ts b/server/src/tests/service.test.ts new file mode 100644 index 0000000..c0d3549 --- /dev/null +++ b/server/src/tests/service.test.ts @@ -0,0 +1,86 @@ +const mockStorageData = new Map(); +jest.mock('../storage', () => ({ + __esModule: true, + default: () => ({ + hasData: async (key: string) => mockStorageData.has(key), + getData: async (key: string) => mockStorageData.get(key) as T, + setData: async (key: string, val: T) => void mockStorageData.set(key, val), + }), + storageReady: Promise.resolve(), +})); + +import { getDateForWeekIndex, getMenuKey, getEmptyData } from '../service'; +import { formatDate } from '../utils'; + +// Pin "today" to 2025-01-10 (Friday, week 2) for deterministic tests +// Monday of that week = 2025-01-06, ..., Friday = 2025-01-10 + +describe('getDateForWeekIndex', () => { + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2025-01-10')); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + test('index 0 (pondělí) vrátí 2025-01-06', () => { + expect(formatDate(getDateForWeekIndex(0))).toBe('2025-01-06'); + }); + + test('index 4 (pátek) vrátí 2025-01-10', () => { + expect(formatDate(getDateForWeekIndex(4))).toBe('2025-01-10'); + }); + + test('index 2 (středa) vrátí 2025-01-08', () => { + expect(formatDate(getDateForWeekIndex(2))).toBe('2025-01-08'); + }); + + test('neplatný index (-1) vrátí dnešek bez vyhození chyby', () => { + const result = getDateForWeekIndex(-1); + expect(result).toBeInstanceOf(Date); + }); + + test('neplatný index (5) vrátí dnešek bez vyhození chyby', () => { + const result = getDateForWeekIndex(5); + expect(result).toBeInstanceOf(Date); + }); +}); + +describe('getMenuKey', () => { + test('vrátí klíč ve tvaru menu_RRRR_TT', () => { + const date = new Date('2025-01-10'); + const key = getMenuKey(date); + expect(key).toMatch(/^menu_\d{4}_\d+$/); + }); + + test('dvě data ve stejném týdnu mají stejný klíč', () => { + expect(getMenuKey(new Date('2025-01-06'))).toBe(getMenuKey(new Date('2025-01-10'))); + }); + + test('dvě data z různých týdnů mají různé klíče', () => { + expect(getMenuKey(new Date('2025-01-06'))).not.toBe(getMenuKey(new Date('2025-01-13'))); + }); +}); + +describe('getEmptyData', () => { + test('vrátí strukturu s prázdnými choices', () => { + const data = getEmptyData(new Date('2025-01-10')); + expect(data.choices).toEqual({}); + }); + + test('vrátí dayIndex=4 pro pátek', () => { + const data = getEmptyData(new Date('2025-01-10')); + expect(data.dayIndex).toBe(4); + }); + + test('isWeekend=false pro pracovní den', () => { + const data = getEmptyData(new Date('2025-01-10')); + expect(data.isWeekend).toBe(false); + }); + + test('isWeekend=true pro víkend', () => { + const data = getEmptyData(new Date('2025-01-11')); + expect(data.isWeekend).toBe(true); + }); +}); diff --git a/server/src/tests/setupEnv.ts b/server/src/tests/setupEnv.ts new file mode 100644 index 0000000..4b6d0c9 --- /dev/null +++ b/server/src/tests/setupEnv.ts @@ -0,0 +1,5 @@ +process.env.NODE_ENV = 'test'; +process.env.STORAGE = 'memory'; +process.env.JWT_SECRET = 'test-jwt-secret-ktery-ma-alespon-32-znaku'; +process.env.LOGOUT_URL = 'http://localhost/logout'; +delete process.env.MOCK_DATA; diff --git a/server/src/tests/statsRoutes.test.ts b/server/src/tests/statsRoutes.test.ts new file mode 100644 index 0000000..23f338c --- /dev/null +++ b/server/src/tests/statsRoutes.test.ts @@ -0,0 +1,60 @@ +import express from 'express'; +import request from 'supertest'; +import bodyParser from 'body-parser'; +import { generateToken } from '../auth'; +import { resetMemoryStorage } from '../storage/memory'; +import statsRouter from '../routes/statsRoutes'; + +function buildApp() { + const app = express(); + app.use(bodyParser.json()); + app.use('/api/stats', statsRouter); + return app; +} + +const TOKEN = `Bearer ${generateToken('testuser')}`; + +beforeEach(() => { + resetMemoryStorage(); +}); + +test('GET /stats bez parametrů vrátí 400', async () => { + const res = await request(buildApp()) + .get('/api/stats') + .set('Authorization', TOKEN); + expect(res.status).toBe(400); +}); + +test('GET /stats s rozsahem 4 dní vrátí 200', async () => { + const res = await request(buildApp()) + .get('/api/stats') + .query({ startDate: '2024-01-08', endDate: '2024-01-12' }) + .set('Authorization', TOKEN); + expect(res.status).toBe(200); + expect(Array.isArray(res.body)).toBe(true); +}); + +test('GET /stats s rozsahem nad 4 dní vrátí 400', async () => { + const res = await request(buildApp()) + .get('/api/stats') + .query({ startDate: '2024-01-01', endDate: '2024-01-10' }) + .set('Authorization', TOKEN); + expect(res.status).toBe(400); +}); + +test('GET /stats s budoucím datem vrátí 400', async () => { + const futureStart = '2099-01-01'; + const futureEnd = '2099-01-05'; + const res = await request(buildApp()) + .get('/api/stats') + .query({ startDate: futureStart, endDate: futureEnd }) + .set('Authorization', TOKEN); + expect(res.status).toBe(400); +}); + +test('GET /stats bez tokenu vrátí chybu', async () => { + const res = await request(buildApp()) + .get('/api/stats') + .query({ startDate: '2024-01-08', endDate: '2024-01-12' }); + expect(res.status).toBeGreaterThanOrEqual(400); +}); diff --git a/server/src/tests/storage-contract.test.ts b/server/src/tests/storage-contract.test.ts new file mode 100644 index 0000000..cf8717b --- /dev/null +++ b/server/src/tests/storage-contract.test.ts @@ -0,0 +1,85 @@ +import * as os from 'os'; +import * as path from 'path'; +import * as fs from 'fs'; +import { StorageInterface } from '../storage/StorageInterface'; +import { resetMemoryStorage } from '../storage/memory'; +import MemoryStorage from '../storage/memory'; +import JsonStorage from '../storage/json'; + +const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'luncher-test-')); +const tempDbPath = path.join(tempDir, 'test-db.json'); + +// Parametrické spuštění stejné sady testů pro obě implementace +const implementations: [string, () => StorageInterface, () => void][] = [ + ['MemoryStorage', () => new MemoryStorage(), resetMemoryStorage], + ['JsonStorage', () => { + // Zajistíme čistý stav souboru před každým testem + if (fs.existsSync(tempDbPath)) { + fs.unlinkSync(tempDbPath); + } + // JsonStorage načte/vytvoří soubor při inicializaci, musíme obalit + const JsonStorageDynamic = require('../storage/json').default; + // Přepíšeme dbPath přes prototyp – pro testy použijeme tmpdir + const inst = Object.create(JsonStorageDynamic.prototype); + const JSONdb = require('simple-json-db'); + (inst as any).db = new JSONdb(tempDbPath); + inst.hasData = async (key: string) => Promise.resolve((inst as any).db.has(key)); + inst.getData = async (key: string) => (inst as any).db.get(key); + inst.setData = async (key: string, data: any) => { (inst as any).db.set(key, data); return Promise.resolve(); }; + return inst; + }, () => { + if (fs.existsSync(tempDbPath)) { + fs.unlinkSync(tempDbPath); + } + }], +]; + +describe.each(implementations)('%s splňuje StorageInterface kontrakt', (name, factory, reset) => { + let storage: StorageInterface; + + beforeEach(() => { + reset(); + storage = factory(); + }); + + test('hasData vrátí false pro neexistující klíč', async () => { + expect(await storage.hasData('neexistujici')).toBe(false); + }); + + test('setData + hasData vrátí true', async () => { + await storage.setData('klic', { value: 1 }); + expect(await storage.hasData('klic')).toBe(true); + }); + + test('setData + getData vrátí uložená data', async () => { + const data = { name: 'Jan', score: 42 }; + await storage.setData('testkey', data); + const result = await storage.getData('testkey'); + expect(result).toEqual(data); + }); + + test('getData pro neexistující klíč vrátí undefined', async () => { + const result = await storage.getData('neexistujici'); + expect(result).toBeUndefined(); + }); + + test('setData přepíše existující data', async () => { + await storage.setData('klic', { version: 1 }); + await storage.setData('klic', { version: 2 }); + const result = await storage.getData<{ version: number }>('klic'); + expect(result?.version).toBe(2); + }); + + test('různé klíče jsou nezávislé', async () => { + await storage.setData('a', { val: 'A' }); + await storage.setData('b', { val: 'B' }); + expect((await storage.getData<{ val: string }>('a'))?.val).toBe('A'); + expect((await storage.getData<{ val: string }>('b'))?.val).toBe('B'); + }); +}); + +afterAll(() => { + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true }); + } +}); diff --git a/server/src/tests/utils.test.ts b/server/src/tests/utils.test.ts new file mode 100644 index 0000000..b5d4846 --- /dev/null +++ b/server/src/tests/utils.test.ts @@ -0,0 +1,90 @@ +import { formatDate, getUsersByLocation, parseToken, checkQueryParams, checkBodyParams, getIsWeekend } from '../utils'; + +describe('formatDate', () => { + const d = new Date('2025-01-10'); + + test('výchozí formát YYYY-MM-DD', () => { + expect(formatDate(d)).toBe('2025-01-10'); + }); + + test('vlastní formát DD.MM.YYYY', () => { + expect(formatDate(d, 'DD.MM.YYYY')).toBe('10.01.2025'); + }); + + test('nulové doplnění dne a měsíce', () => { + expect(formatDate(new Date('2025-03-05'))).toBe('2025-03-05'); + }); +}); + +describe('getIsWeekend', () => { + test('pondělí není víkend', () => { + expect(getIsWeekend(new Date('2025-01-06'))).toBe(false); + }); + test('pátek není víkend', () => { + expect(getIsWeekend(new Date('2025-01-10'))).toBe(false); + }); + test('sobota je víkend', () => { + expect(getIsWeekend(new Date('2025-01-11'))).toBe(true); + }); + test('neděle je víkend', () => { + expect(getIsWeekend(new Date('2025-01-12'))).toBe(true); + }); +}); + +describe('getUsersByLocation', () => { + const choices = { + SLADOVNICKA: { alice: { trusted: false, selectedFoods: [] } }, + TECHTOWER: { bob: { trusted: true, selectedFoods: [] } }, + } as any; + + test('vrátí spolužáky ze stejného místa', () => { + expect(getUsersByLocation(choices, 'alice')).toEqual(['alice']); + }); + + test('vrátí prázdné pole pro neznámý login', () => { + expect(getUsersByLocation(choices, 'charlie')).toEqual([]); + }); + + test('vrátí prázdné pole pro chybějící login', () => { + expect(getUsersByLocation(choices, undefined)).toEqual([]); + }); +}); + +describe('parseToken', () => { + test('vrátí token z Authorization hlavičky', () => { + const req = { headers: { authorization: 'Bearer mytoken' } }; + expect(parseToken(req)).toBe('mytoken'); + }); + + test('vrátí undefined pro chybějící hlavičku', () => { + expect(parseToken({ headers: {} })).toBeUndefined(); + }); + + test('vrátí undefined pro chybějící req', () => { + expect(parseToken(undefined)).toBeUndefined(); + }); +}); + +describe('checkQueryParams', () => { + test('nevyhodí chybu pro přítomné parametry', () => { + const req = { query: { date: '2025-01-10', location: 'SLADOVNICKA' } }; + expect(() => checkQueryParams(req, ['date', 'location'])).not.toThrow(); + }); + + test('vyhodí chybu pro chybějící parametr', () => { + const req = { query: { date: '2025-01-10' } }; + expect(() => checkQueryParams(req, ['date', 'location'])).toThrow("'location'"); + }); +}); + +describe('checkBodyParams', () => { + test('nevyhodí chybu pro přítomné parametry', () => { + const req = { body: { login: 'alice' } }; + expect(() => checkBodyParams(req, ['login'])).not.toThrow(); + }); + + test('vyhodí chybu pro chybějící parametr', () => { + const req = { body: {} }; + expect(() => checkBodyParams(req, ['login'])).toThrow("'login'"); + }); +}); diff --git a/server/src/tests/voting.test.ts b/server/src/tests/voting.test.ts new file mode 100644 index 0000000..d25e294 --- /dev/null +++ b/server/src/tests/voting.test.ts @@ -0,0 +1,77 @@ +import { getUserVotes, updateFeatureVote, getVotingStats } from '../voting'; +import { resetMemoryStorage } from '../storage/memory'; +import { FeatureRequest } from '../../../types/gen/types.gen'; + +const OPT_A = FeatureRequest.STATISTICS; +const OPT_B = FeatureRequest.UI; + +beforeEach(() => { + resetMemoryStorage(); +}); + +describe('updateFeatureVote', () => { + test('přidá hlas pro nového uživatele', async () => { + const result = await updateFeatureVote('alice', OPT_A, true); + expect(result['alice']).toContain(OPT_A); + }); + + test('vyhodí chybu při duplicitním hlasování', async () => { + await updateFeatureVote('alice', OPT_A, true); + await expect(updateFeatureVote('alice', OPT_A, true)).rejects.toThrow('hlasovali'); + }); + + test('odebere hlas', async () => { + await updateFeatureVote('alice', OPT_A, true); + await updateFeatureVote('alice', OPT_A, false); + const stats = await getVotingStats(); + expect(stats[OPT_A] ?? 0).toBe(0); + }); + + test('odebrání neexistujícího hlasu je no-op', async () => { + await expect(updateFeatureVote('alice', OPT_A, false)).resolves.not.toThrow(); + }); + + test('odebrání posledního hlasu odstraní login ze storage', async () => { + await updateFeatureVote('alice', OPT_A, true); + const data = await updateFeatureVote('alice', OPT_A, false); + expect('alice' in data).toBe(false); + }); + + test('vyhodí chybu po 4 hlasech', async () => { + const options = Object.values(FeatureRequest); + for (let i = 0; i < 4; i++) { + await updateFeatureVote('alice', options[i], true); + } + await expect(updateFeatureVote('alice', options[4], true)).rejects.toThrow('4'); + }); +}); + +describe('getUserVotes', () => { + test('vrátí hlasy uživatele', async () => { + await updateFeatureVote('alice', OPT_A, true); + const votes = await getUserVotes('alice'); + expect(votes).toContain(OPT_A); + }); + + test('vrátí prázdné pole pro uživatele bez hlasů', async () => { + const votes = await getUserVotes('neexistujici'); + expect(votes).toEqual([]); + }); +}); + +describe('getVotingStats', () => { + test('vrátí agregované počty hlasů', async () => { + await updateFeatureVote('alice', OPT_A, true); + await updateFeatureVote('bob', OPT_A, true); + await updateFeatureVote('bob', OPT_B, true); + + const stats = await getVotingStats(); + expect(stats[OPT_A]).toBe(2); + expect(stats[OPT_B]).toBe(1); + }); + + test('vrátí prázdný objekt bez hlasů', async () => { + const stats = await getVotingStats(); + expect(stats).toEqual({}); + }); +}); diff --git a/server/src/tests/votingRoutes.test.ts b/server/src/tests/votingRoutes.test.ts new file mode 100644 index 0000000..46cad73 --- /dev/null +++ b/server/src/tests/votingRoutes.test.ts @@ -0,0 +1,76 @@ +import express from 'express'; +import request from 'supertest'; +import bodyParser from 'body-parser'; +import { generateToken } from '../auth'; +import { resetMemoryStorage } from '../storage/memory'; +import { FeatureRequest } from '../../../types/gen/types.gen'; +import votingRouter from '../routes/votingRoutes'; + +const VALID_OPTION = FeatureRequest.STATISTICS; + +function buildApp() { + const app = express(); + app.use(bodyParser.json()); + app.use('/api/voting', votingRouter); + app.use((err: any, _req: any, res: any, _next: any) => { + res.status(400).json({ error: err.message }); + }); + return app; +} + +const TOKEN = `Bearer ${generateToken('testuser')}`; + +beforeEach(() => { + resetMemoryStorage(); +}); + +test('GET /getVotes vrátí 200 s prázdným polem pro nového uživatele', async () => { + const res = await request(buildApp()) + .get('/api/voting/getVotes') + .set('Authorization', TOKEN); + expect(res.status).toBe(200); + expect(res.body).toEqual([]); +}); + +test('GET /getVotes vrátí 401 bez tokenu', async () => { + const res = await request(buildApp()).get('/api/voting/getVotes'); + expect(res.status).toBeGreaterThanOrEqual(400); +}); + +test('POST /updateVote přidá hlas a vrátí 200', async () => { + const res = await request(buildApp()) + .post('/api/voting/updateVote') + .set('Authorization', TOKEN) + .send({ option: VALID_OPTION, active: true }); + expect(res.status).toBe(200); +}); + +test('POST /updateVote vrátí 400 pro chybějící parametry', async () => { + const res = await request(buildApp()) + .post('/api/voting/updateVote') + .set('Authorization', TOKEN) + .send({}); + expect(res.status).toBe(400); +}); + +test('POST /updateVote vrátí 400 při duplicitním hlasu', async () => { + const app = buildApp(); + await request(app) + .post('/api/voting/updateVote') + .set('Authorization', TOKEN) + .send({ option: VALID_OPTION, active: true }); + const res = await request(app) + .post('/api/voting/updateVote') + .set('Authorization', TOKEN) + .send({ option: VALID_OPTION, active: true }); + expect(res.status).toBe(400); + expect(res.body.error).toContain('hlasovali'); +}); + +test('GET /stats vrátí 200 s objektem', async () => { + const res = await request(buildApp()) + .get('/api/voting/stats') + .set('Authorization', TOKEN); + expect(res.status).toBe(200); + expect(typeof res.body).toBe('object'); +}); diff --git a/server/tsconfig.json b/server/tsconfig.json index 49e80bb..813b1b1 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -3,6 +3,9 @@ "src/**/*", "../types/**/*" ], + "exclude": [ + "src/tests/**/*" + ], "compilerOptions": { "target": "ES2022", "module": "Node16", diff --git a/server/yarn.lock b/server/yarn.lock index d4e1fad..719efc3 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -1448,6 +1448,18 @@ "@emnapi/runtime" "^1.4.3" "@tybys/wasm-util" "^0.10.0" +"@noble/hashes@^1.1.5": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" + integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== + +"@paralleldrive/cuid2@^2.2.2": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz#3d62ea9e7be867d3fa94b9897fab5b0ae187d784" + integrity sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw== + dependencies: + "@noble/hashes" "^1.1.5" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -1594,6 +1606,11 @@ dependencies: "@types/node" "*" +"@types/cookiejar@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" + integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== + "@types/cors@^2.8.12": version "2.8.19" resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.19.tgz#d93ea2673fd8c9f697367f5eeefc2bbfa94f0342" @@ -1660,6 +1677,11 @@ "@types/ms" "*" "@types/node" "*" +"@types/methods@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@types/methods/-/methods-1.1.4.tgz#d3b7ac30ac47c91054ea951ce9eed07b1051e547" + integrity sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ== + "@types/ms@*": version "2.1.0" resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" @@ -1727,6 +1749,24 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== +"@types/superagent@^8.1.0": + version "8.1.9" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-8.1.9.tgz#28bfe4658e469838ed0bf66d898354bcab21f49f" + integrity sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ== + dependencies: + "@types/cookiejar" "^2.1.5" + "@types/methods" "^1.1.4" + "@types/node" "*" + form-data "^4.0.0" + +"@types/supertest@^6.0.0": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-6.0.3.tgz#d736f0e994b195b63e1c93e80271a2faf927388c" + integrity sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w== + dependencies: + "@types/methods" "^1.1.4" + "@types/superagent" "^8.1.0" + "@types/tough-cookie@*": version "4.0.5" resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" @@ -1940,6 +1980,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + asn1.js@^5.3.0: version "5.4.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" @@ -2294,6 +2339,11 @@ combined-stream@^1.0.6, combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +component-emitter@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17" + integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -2314,7 +2364,7 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== -cookie-signature@^1.2.1: +cookie-signature@^1.2.1, cookie-signature@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== @@ -2324,6 +2374,11 @@ cookie@^0.7.1, cookie@~0.7.2: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== +cookiejar@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" + integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== + core-js-compat@^3.43.0: version "3.47.0" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.47.0.tgz#698224bbdbb6f2e3f39decdda4147b161e3772a3" @@ -2369,7 +2424,7 @@ css-what@^6.1.0: resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== -debug@4, debug@^4, debug@^4.1.0, debug@^4.3.1, debug@^4.4.0, debug@^4.4.1, debug@^4.4.3, debug@~4.4.1: +debug@4, debug@^4, debug@^4.1.0, debug@^4.3.1, debug@^4.3.7, debug@^4.4.0, debug@^4.4.1, debug@^4.4.3, debug@~4.4.1: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -2408,6 +2463,14 @@ detect-newline@^3.1.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== + dependencies: + asap "^2.0.0" + wrappy "1" + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -2681,6 +2744,11 @@ fast-json-stable-stringify@^2.1.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== +fast-safe-stringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + fb-watchman@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" @@ -2738,6 +2806,17 @@ form-data@^2.5.0: mime-types "^2.1.12" safe-buffer "^5.2.1" +form-data@^4.0.0, form-data@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053" + integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + hasown "^2.0.2" + mime-types "^2.1.12" + form-data@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4" @@ -2749,6 +2828,15 @@ form-data@^4.0.4: hasown "^2.0.2" mime-types "^2.1.12" +formidable@^3.5.4: + version "3.5.4" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-3.5.4.tgz#ac9a593b951e829b3298f21aa9a2243932f32ed9" + integrity sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug== + dependencies: + "@paralleldrive/cuid2" "^2.2.2" + dezalgo "^1.0.4" + once "^1.4.0" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -3624,6 +3712,11 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== +methods@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + micromatch@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" @@ -3656,6 +3749,11 @@ mime-types@^3.0.0, mime-types@^3.0.2: dependencies: mime-db "^1.54.0" +mime@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -4337,6 +4435,30 @@ strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +superagent@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-10.3.0.tgz#ff1e39e7976b63f8084291d65f5bfbbbbd156989" + integrity sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ== + dependencies: + component-emitter "^1.3.1" + cookiejar "^2.1.4" + debug "^4.3.7" + fast-safe-stringify "^2.1.1" + form-data "^4.0.5" + formidable "^3.5.4" + methods "^1.1.2" + mime "2.6.0" + qs "^6.14.1" + +supertest@^7.0.0: + version "7.2.2" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-7.2.2.tgz#dac3ee25a2aa59942a7f641e50c838a7c8819204" + integrity sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA== + dependencies: + cookie-signature "^1.2.2" + methods "^1.1.2" + superagent "^10.3.0" + supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" diff --git a/types/api.yml b/types/api.yml index dc0dab3..8b5a570 100644 --- a/types/api.yml +++ b/types/api.yml @@ -77,6 +77,10 @@ paths: /voting/stats: $ref: "./paths/voting/getVotingStats.yml" + # Changelog (/api/changelogs) + /changelogs: + $ref: "./paths/changelogs/getChangelogs.yml" + # DEV endpointy (/api/dev) /dev/generate: $ref: "./paths/dev/generate.yml" diff --git a/types/paths/changelogs/getChangelogs.yml b/types/paths/changelogs/getChangelogs.yml new file mode 100644 index 0000000..3180b86 --- /dev/null +++ b/types/paths/changelogs/getChangelogs.yml @@ -0,0 +1,21 @@ +get: + operationId: getChangelogs + summary: Vrátí seznam změn (changelog). Pokud není předáno datum, vrátí všechny změny. Pokud je předáno datum, vrátí pouze změny po tomto datu. + parameters: + - in: query + name: since + required: false + schema: + type: string + description: Datum (formát YYYY-MM-DD) od kterého se mají vrátit změny (exkluzivně). Pokud není předáno, vrátí se všechny změny. + responses: + "200": + description: Slovník kde klíčem je datum (YYYY-MM-DD) a hodnotou seznam změn k danému datu. Seřazeno od nejnovějšího po nejstarší. + content: + application/json: + schema: + type: object + additionalProperties: + type: array + items: + type: string diff --git a/types/paths/getPizzaQr.yml b/types/paths/getPizzaQr.yml index ba7ba49..81e69dc 100644 --- a/types/paths/getPizzaQr.yml +++ b/types/paths/getPizzaQr.yml @@ -1,6 +1,6 @@ get: operationId: getPizzaQr - summary: Získání QR kódu pro platbu za Pizza day + summary: Získání QR kódu pro platbu security: [] # Nevyžaduje autentizaci parameters: - in: query @@ -9,6 +9,12 @@ get: type: string required: true description: Přihlašovací jméno uživatele, pro kterého bude vrácen QR kód + - in: query + name: id + schema: + type: string + required: true + description: Unikátní identifikátor QR kódu (z PendingQr.id) responses: "200": description: Vygenerovaný QR kód pro platbu diff --git a/types/paths/pizzaDay/addPizza.yml b/types/paths/pizzaDay/addPizza.yml index fed8511..9637580 100644 --- a/types/paths/pizzaDay/addPizza.yml +++ b/types/paths/pizzaDay/addPizza.yml @@ -1,21 +1,21 @@ post: operationId: addPizza - summary: Přidání pizzy do objednávky. + summary: Přidání pizzy nebo salátu do objednávky. requestBody: required: true content: application/json: schema: - required: - - pizzaIndex - - pizzaSizeIndex properties: pizzaIndex: - description: Index pizzy v nabídce + description: Index pizzy v nabídce (pro přidání pizzy) type: integer pizzaSizeIndex: - description: Index velikosti pizzy v nabídce variant + description: Index velikosti pizzy v nabídce variant (pro přidání pizzy) + type: integer + salatIndex: + description: Index salátu v nabídce (pro přidání salátu) type: integer responses: "200": - description: Přidání pizzy do objednávky proběhlo úspěšně. + description: Přidání pizzy nebo salátu do objednávky proběhlo úspěšně. diff --git a/types/paths/pizzaDay/dismissQr.yml b/types/paths/pizzaDay/dismissQr.yml index ffa95bb..0aa6fba 100644 --- a/types/paths/pizzaDay/dismissQr.yml +++ b/types/paths/pizzaDay/dismissQr.yml @@ -1,17 +1,17 @@ post: operationId: dismissQr - summary: Označí QR kód pro daný den jako uhrazený (odstraní ho ze seznamu nevyřízených). + summary: Označí QR kód jako uhrazený (odstraní ho ze seznamu nevyřízených). requestBody: required: true content: application/json: schema: properties: - date: - description: Datum Pizza day, ke kterému se QR kód vztahuje + id: + description: Unikátní identifikátor QR kódu (z PendingQr.id) type: string required: - - date + - id responses: "200": description: QR kód byl označen jako uhrazený. diff --git a/types/schemas/_index.yml b/types/schemas/_index.yml index 9e4b3a8..19d7808 100644 --- a/types/schemas/_index.yml +++ b/types/schemas/_index.yml @@ -53,6 +53,11 @@ ClientData: description: Datum a čas poslední aktualizace pizz type: string format: date-time + salatList: + description: Seznam dostupných salátů pro předaný den + type: array + items: + $ref: "#/Salat" pendingQrs: description: Nevyřízené QR kódy pro platbu z předchozích pizza day type: array @@ -198,6 +203,9 @@ RestaurantDayMenu: type: array items: type: string + isStale: + description: Příznak, zda data mohou pocházet z jiného týdne + type: boolean RestaurantDayMenuMap: description: Objekt, kde klíčem je podnik ((#Restaurant)) a hodnotou denní menu daného podniku ((#RestaurantDayMenu)) type: object @@ -435,7 +443,7 @@ Pizza: items: $ref: "#/PizzaSize" PizzaVariant: - description: Konkrétní varianta (velikost) jedné pizzy. + description: Konkrétní varianta (velikost) jedné pizzy nebo salátu. type: object additionalProperties: false required: @@ -445,16 +453,40 @@ PizzaVariant: - price properties: varId: - description: Unikátní identifikátor varianty pizzy + description: Unikátní identifikátor varianty type: integer name: - description: Název pizzy + description: Název pizzy nebo salátu type: string size: - description: Velikost pizzy (např. "30cm") + description: Velikost pizzy (např. "30cm"), nebo "1 porce" pro salát type: string price: - description: Cena pizzy v Kč, včetně krabice + description: Cena v Kč, včetně krabice/obalu + type: number + category: + description: Kategorie položky (pizza nebo salat) + type: string + enum: [pizza, salat] +Salat: + description: Salát z nabídky Pizza Chefie + type: object + additionalProperties: false + required: + - name + - ingredients + - price + properties: + name: + description: Název salátu + type: string + ingredients: + description: Seznam obsažených ingrediencí + type: array + items: + type: string + price: + description: Cena salátu v Kč (bez obalu) type: number PizzaOrder: description: Údaje o objednávce pizzy jednoho uživatele. @@ -644,19 +676,23 @@ ClearMockDataRequest: # --- NEVYŘÍZENÉ QR KÓDY --- PendingQr: - description: Nevyřízený QR kód pro platbu z předchozího Pizza day + description: Nevyřízený QR kód pro platbu type: object additionalProperties: false required: + - id - date - creator - totalPrice properties: + id: + description: Unikátní identifikátor QR kódu (umožňuje více QR na strávníka na den) + type: string date: - description: Datum Pizza day, ke kterému se QR kód vztahuje + description: Datum, ke kterému se QR kód vztahuje type: string creator: - description: Jméno zakladatele Pizza day (objednávajícího) + description: Jméno uživatele, který QR vygeneroval (příjemce platby) type: string totalPrice: description: Celková cena objednávky v Kč