commit a5711f3e1e270583579a7f4faf0dab7d11cbb2a6 Author: Ondřej Anděl Date: Mon Sep 7 14:58:49 2026 +0200 feat: založení základní stránky příjem/výdej + import a statistiky diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9585019 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +node_modules +types/gen +**.DS_Store +.mcp.json +.claude/settings.local.json +server/public/ +server/data/ +.claude/*.lock +.claude/worktrees +.playwright-mcp +.idea/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..24607f3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,190 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Food Tracer je aplikace pro evidenci snědených jídel a útraty za jídlo. Czech-language UI. +Full-stack TypeScript monorepo. Technologicky i vzhledově navazuje na sesterský projekt +**Luncher** (`../Luncher`) — sdílí s ním stack, konvence i design systém. + +## Monorepo Structure + +``` +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) +``` + +Package manager: **Yarn Classic**. Deployment: `Dockerfile`, `compose.yml`. + +## Development Commands + +### Initial setup +```bash +cd types && yarn install && yarn openapi-ts # Generate API types first +cd ../server && yarn install +cd ../client && yarn install +``` + +### Running dev environment +```bash +./run_dev.sh # tmux (Linux/macOS) +.\run_dev.ps1 # dvě PowerShell okna (Windows) + +# Nebo ručně: +cd server && NODE_ENV=development yarn startReload # Port 3001 +cd client && yarn start # Port 3000, proxies /api → 3001 +``` + +### Building & tests +```bash +cd types && yarn openapi-ts # Regenerate types from api.yml +cd server && yarn build # tsc → server/dist +cd server && yarn test # Jest (in-memory storage) +cd server && yarn test meals # Run one file by name +cd client && yarn build # tsc --noEmit + vite build → client/dist +``` + +## Architecture + +### API Types (types/) +- OpenAPI 3.0 spec in `types/api.yml` — thin aggregator, endpoint specs 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, auth middleware, error middleware +- **Routes:** `routes/` — `dayRoutes`, `mealRoutes`, `activityRoutes`, `workoutRoutes`, + `statsRoutes`, `calorieRoutes`, `settingsRoutes`, `importRoutes` +- **Domain:** `meals.ts` (jídlo), `activities.ts` (pohyb), `workouts.ts` (šablony + tréninků), `dayOverview.ts` (spojení dne a bilance), `statsService.ts` (agregace), + `luncherImport.ts` (parsování exportů z Luncheru), `settings.ts`, `calories.ts` +- **Auth:** `auth.ts` — JWT + volitelná autentizace z hlavičky reverzní proxy +- **Storage:** `storage/index.ts` factory dle proměnné `STORAGE`; backendy: + `json.ts` (soubor, vývoj), `redis.ts` (produkce), `memory.ts` (testy) +- **Config:** `.env.development` / `.env.production` (viz `.env.template`) + +### Client (client/src/) +- **Entry:** `index.tsx` → `AppRoutes.tsx`; `Login.tsx` je přihlašovací obrazovka +- **Pages:** `pages/` — `DayPage` (přehled dne se záložkami Příjem/Výdej), + `StatsPage` (statistiky + import) +- **Components:** `components/` (Header, CalorieLookup) a `components/modals/` + (MealModal, ActivityModal, WorkoutModal, SettingsModal, ImportModal) +- **Context:** `context/auth.tsx` (JWT), `context/settings.tsx` (světlý/tmavý motiv) +- **Routing:** konstanty adres jsou v `routes.ts`, ne v `AppRoutes.tsx` — hlavička + je potřebuje a kruhový import by je nechal nedefinované +- **Styling:** Bootstrap 5 + React Bootstrap + SCSS; design systém a proměnné + `--ft-*` jsou v `App.scss`, stránkové styly co-located vedle komponent +- **API:** přes OpenAPI SDK z `types/gen/` + +## Data model + +- Jídlo (`MealEntry`) patří jednomu dni a jednomu uživateli; klíč úložiště je + `meals::` a hodnotou je pole záznamů dne. +- **Ceny jsou všude celá čísla v haléřích** — v úložišti, v API i mezi klientem + a serverem. Na koruny se převádí až při zobrazení (`formatPrice` v `Utils.tsx`). + Stejnou konvenci má Luncher, díky tomu se částky z importu přenesou beze ztráty. +- Kalorie jsou volitelné celé číslo v kcal. Jídla bez kalorií se do součtů + nezapočítávají. +- Gramáž (`weight`, g) a jednotkové hodnoty (`pricePer100g` v haléřích za 100 g, + `caloriesPer100g` v kcal) umožňují dopočet — viz níže. + +## Gramáž a kalorie + +`deriveAmounts` v `server/src/meals.ts` je jediný zdroj pravdy pro dopočty: + +1. **gramáž** = `price / pricePer100g * 100`, pokud není zadaná ručně +2. **kalorie** = `weight / 100 * caloriesPer100g`, jinak ručně zadaná hodnota + +Ručně zadaná gramáž má vždy přednost — porci lze zvážit přesněji, než kolik řekne +cena. Sazba je na každém záznamu zvlášť, takže hlavní jídlo za 44 Kč/100 g a salát +s jiným cenováním můžou být ve stejném dni vedle sebe. Výchozí sazby podniků drží +`server/src/settings.ts` (`sourceRates`) a klient jimi předvyplňuje pole, když +uživatel vybere zdroj. + +`client/src/Utils.tsx` má stejnojmennou funkci pro živý náhled ve formuláři. +**Když se změní jedna, musí se změnit i druhá** — jinak klient ukazuje něco jiného, +než server uloží. + +## Zdroje energetických hodnot + +`server/src/calorieProvider.ts` definuje rozhraní `CalorieProvider` a jeho +implementaci nad Open Food Facts. Poskytovatel je záměrně vyměnitelný za jeden +soubor a **nikdy nevyhazuje výjimku** — při nedostupnosti vrací `unavailable`, +takže hledání jen přijde o návrhy a nespadne. `CALORIE_PROVIDER=none` externí +dotazy vypne (offline provoz, testy). + +Poznámky ke zdrojům, ověřené v září 2026: + +- **KalorickéTabulky.cz nemají veřejné API.** Jejich robots.txt zakazuje + `/*query.page` (jejich vyhledávání) a smluvní podmínky omezují užití nad rámec + zamýšleného účelu, takže se odtud **nesmí scrapovat**. Aplikace jen nabídne + odkaz na jejich tabulku potravin a zkopíruje název jídla do schránky. + Předvyplnit jejich hledání z URL nejde — běží v JavaScriptu a parametry v query + stringu ignoruje (ověřeno porovnáním odpovědí). +- **Open Food Facts:** používá se `search.openfoodfacts.org`, ne hlavní + `world.openfoodfacts.org` — tamní `/cgi/search.pl` i `/api/v2/search` vrací 503. + Produkty bez `energy-kcal_100g` se odfiltrují. +- **Vlastní knihovna** (`server/src/calories.ts`) si pamatuje kcal/100 g pod + znormalizovaným názvem jídla a řadí se před externí návrhy — na kantýnová jídla + sedí líp než databáze balených potravin. Plní se sama při uložení jídla. + +## Pohyb a energetická bilance + +Den má dvě strany a `DayPage` je dělí do záložek: + +- **Příjem** — jídla (`MealEntry`), klíč `meals::` +- **Výdej** — pohyb (`ActivityEntry`), klíč `activities::` + +Aktivita se měří v jednotce (`ActivityUnit`: KROKY, MINUTY, KM, OPAKOVANI) a +kalorie se dopočtou jako `quantity / 100 * caloriesPer100Units`. **Sto jednotek, +ne jedna** — u kroků by sazba na jeden krok byla zlomek (~0,04 kcal) a aplikace +všude pracuje s celými čísly. Stejná konvence jako u energie jídla na 100 g. + +`WorkoutTemplate` (`workouts.ts`) je pojmenovaný seznam cviků. Použitím vzniknou +běžné aktivity s vazbou `templateId` — jsou samostatné, takže úprava založené +položky šablonu nemění. + +`buildEnergyBalance` v `dayOverview.ts` počítá: +`bilance = příjem − (klidový výdej + pohyb)`. Záporná hodnota je deficit. +Bez nastaveného klidového výdeje (`basalCalories` v nastavení) porovnává bilance +jen jídlo proti pohybu — **to není skutečný deficit**, proto se to přes +`hasBasal: false` propisuje do UI, aby to číslo nikoho nemátlo. + +`GET /api/day?date=` vrací `DayOverview` se vším naráz (jídlo, pohyb, bilance), +takže `DayPage` si vystačí s jedním voláním. + +## Import z Luncheru + +`server/src/luncherImport.ts` čte měsíční přehled ze stránky statistik Luncheru +ve všech třech formátech, které Luncher exportuje (XLSX, CSV, JSON — viz +`../Luncher/server/src/userExport.ts`). + +- Sloupce se hledají **podle názvu hlavičky**, ne podle pozice. +- CSV má BOM, oddělovač `;`, datum `DD.MM.YYYY` a desetinnou čárku (český Excel). +- XLSX má dva listy — čte se `Přehled`, list `Souhrn` se ignoruje. +- Řádky se zakládají jako **oběd**, pokud se neurčí jinak (Luncher řeší obědy). +- `name` = jídlo → poznámka → typ záznamu. U voleb "Budu objednávat" a + "Rozhoduji se" bývá sloupec s jídlem prázdný a co se reálně jedlo stojí + v poznámce ("Chefie - Těstovinový salát"), proto ten mezikrok. Poznámka, ze + které se stal název, se už nekopíruje do `note`. +- `source` = obchod objednávky, jinak podnik. Stavy volby z `NON_PLACE_TYPES` + ("Budu objednávat", "Rozhoduji se", "Objednávka", "Mám vlastní/neobědvám") + nejsou místa, takže se jako zdroj nepoužijí a záznam zůstane bez zdroje. +- Duplicity řeší `importKey` složený z data, pořadí řádku v rámci dne, typu, + jídla a částky — opakovaný import stejného měsíce nic nezduplikuje. +- Endpoint podporuje `dryRun` pro náhled před uložením; klient ho vždy použije. + +**Když se změní formát exportu v Luncheru, je potřeba upravit i tento parser** +a testy v `server/src/tests/luncherImport.test.ts`, které si export sestavují +přesně tak, jak ho Luncher generuje. + +## Conventions + +- 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 +- Komentáře v češtině, u netriviálních míst vysvětlují **proč**, ne co diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fbc43f8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,52 @@ +# syntax=docker/dockerfile:1 + +# ─── Generování typů z OpenAPI ──────────────────────────────────────────────── +FROM node:22-alpine AS types +WORKDIR /app/types +COPY types/package.json types/yarn.lock* ./ +RUN yarn install --frozen-lockfile +COPY types/ ./ +RUN yarn openapi-ts + +# ─── Build serveru ──────────────────────────────────────────────────────────── +FROM node:22-alpine AS server-build +WORKDIR /app +COPY --from=types /app/types ./types +WORKDIR /app/server +COPY server/package.json server/yarn.lock* ./ +RUN yarn install --frozen-lockfile +COPY server/ ./ +RUN yarn build + +# ─── Build klienta ──────────────────────────────────────────────────────────── +FROM node:22-alpine AS client-build +WORKDIR /app +COPY --from=types /app/types ./types +WORKDIR /app/client +COPY client/package.json client/yarn.lock* ./ +RUN yarn install --frozen-lockfile +COPY client/ ./ +RUN yarn build + +# ─── Runtime ────────────────────────────────────────────────────────────────── +FROM node:22-alpine AS runner +ENV NODE_ENV=production +ENV TZ=Europe/Prague +RUN apk add --no-cache tzdata + +WORKDIR /app +# Server běží ze zkompilovaného dist/, kde si drží strukturu server/ + types/ +COPY --from=server-build /app/server/dist ./ +COPY server/package.json ./server/package.json +COPY server/yarn.lock* ./server/ +WORKDIR /app/server +RUN yarn install --frozen-lockfile --production + +# Klient se servíruje jako statické soubory ze složky public +COPY --from=client-build /app/client/dist ./public + +EXPOSE 3001 +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \ + CMD wget -qO- http://127.0.0.1:3001/api/health || exit 1 + +CMD ["node", "src/index.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..bcb60f8 --- /dev/null +++ b/README.md @@ -0,0 +1,105 @@ +# Food Tracer + +Aplikace pro evidenci snědených jídel a útraty za jídlo. Ke každému jídlu se dá +vedle názvu a zdroje uložit i cena a kalorie, takže jde zpětně zjistit, kolik +člověk za jídlo utratil za den, měsíc nebo rok. + +Umí naimportovat měsíční přehled vyexportovaný ze stránky statistik +[Luncheru](../Luncher) — ve formátu XLSX, CSV i JSON. + +## Struktura + +``` +types/ → Sdílené TypeScript typy generované z OpenAPI (zdroj pravdy: types/api.yml) +server/ → Express 5 backend (Node.js 22, ts-node) +client/ → React 19 frontend (Vite 7, React Bootstrap) +``` + +Package manager: **Yarn Classic**. Každá složka má vlastní `package.json`. + +## Rozjetí + +```bash +cd types && yarn install && yarn openapi-ts # typy je nutné vygenerovat první +cd ../server && yarn install +cd ../client && yarn install +``` + +Do `server/.env.development` patří `JWT_SECRET` o délce alespoň 32 znaků +(šablona je v `server/.env.template`). + +## Vývoj + +```bash +./run_dev.sh # vše naráz v tmuxu (Linux/macOS) +.\run_dev.ps1 # dvě PowerShell okna (Windows) +``` + +Nebo ručně ve dvou terminálech: + +```bash +cd server && NODE_ENV=development yarn startReload # port 3001 +cd client && yarn start # port 3000, proxy /api → 3001 +``` + +## Testy a build + +```bash +cd server && yarn test # Jest, in-memory úložiště +cd server && yarn build # tsc → server/dist +cd client && yarn build # tsc --noEmit + vite build → client/dist +``` + +## Nasazení + +```bash +JWT_SECRET=... docker compose up --build +``` + +Produkčně běží aplikace nad Redisem, ve vývoji nad JSON souborem +(`server/data/db.json`). Viz `server/.env.template`. + +## Den: příjem a výdej + +Přehled dne má dvě záložky: + +- **Příjem** — co jste snědli, s cenou, gramáží a kaloriemi. +- **Výdej** — pohyb. Kroky, minuty, kilometry nebo opakování; z množství a + spotřeby na 100 jednotek se dopočtou spálené kalorie (10 000 kroků ≈ 400 kcal). + +Opakované tréninky se dají uložit jako **šablonu** — *Workout day 1* pak stačí +jedním kliknutím založit do dne celý, místo zadávání cviku po cviku. Založené +položky jsou samostatné, takže je jde doupravit, aniž by se změnila šablona. + +Nahoře je **bilance dne**: `příjem − (klidový výdej + pohyb)`. Klidový výdej +(bazální metabolismus) se nastavuje v *Nastavení* — bez něj se porovnává jen +jídlo proti pohybu a nejde o skutečný deficit, na což aplikace upozorní. + +## Kalorie a gramáž + +Ke každému jídlu jde vyplnit **cenu za 100 g** a z ní se dopočítá **gramáž** +(u TechToweru typicky 44 Kč/100 g). Gramáž jde kdykoli přepsat ručně a každá +položka má vlastní sazbu — hlavní jídlo a salát s jiným cenováním se nepletou. +Sazby podniků se nastavují v *Nastavení* a při výběru zdroje se předvyplní. + +Z gramáže a **energie na 100 g** se pak dopočítají kalorie. Hodnotu nabídne +tlačítko *Najít kalorie*: + +- z **vlastní knihovny** — co jste jednou zadali, aplikace si pamatuje pod názvem + jídla a příště nabídne sama, +- z **Open Food Facts** (otevřená databáze, bez klíče), +- odkazem na **KalorickéTabulky.cz**, které veřejné API nemají — otevřou se + v novém panelu a název jídla se zkopíruje do schránky, hodnotu opíšete ručně. + +Když je externí databáze nedostupná, hledání nespadne — zůstane vlastní knihovna. + +## Import z Luncheru + +Na stránce **Statistiky** je tlačítko *Import z Luncheru*. Stačí nahrát soubor +stažený ze statistik Luncheru: + +- Záznamy se rozdělí do dnů, ke kterým patří (export je typicky za celý měsíc). +- Zakládají se jako **oběd**, protože Luncher řeší výběr obědů. Chod jde při + importu změnit a jednotlivá jídla pak upravit v přehledu dne. +- Před uložením se ukáže náhled toho, co se založí. +- Opakovaný import stejného měsíce data nezduplikuje. diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..036447b --- /dev/null +++ b/client/index.html @@ -0,0 +1,15 @@ + + + + + + + + Food Tracer + + + +
+ + + diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..af03ec3 --- /dev/null +++ b/client/package.json @@ -0,0 +1,38 @@ +{ + "name": "@food-tracer/client", + "version": "0.1.0", + "license": "MIT", + "private": true, + "type": "module", + "homepage": ".", + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^7.1.0", + "@fortawesome/free-regular-svg-icons": "^7.1.0", + "@fortawesome/free-solid-svg-icons": "^7.1.0", + "@fortawesome/react-fontawesome": "^3.1.0", + "@types/node": "^24.10.0", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^5.1.0", + "bootstrap": "^5.3.8", + "react": "^19.2.0", + "react-bootstrap": "^2.10.10", + "react-dom": "^19.2.0", + "react-jwt": "^1.3.0", + "react-router": "^7.9.5", + "react-router-dom": "^7.9.5", + "react-toastify": "^11.0.5", + "recharts": "^3.4.1", + "sass": "^1.93.3", + "typescript": "^5.9.3", + "vite": "^7.2.2", + "vite-tsconfig-paths": "^5.1.4" + }, + "scripts": { + "start": "vite", + "build": "tsc --noEmit && vite build" + }, + "devDependencies": { + "prettier": "^3.6.2" + } +} diff --git a/client/src/App.scss b/client/src/App.scss new file mode 100644 index 0000000..720bf00 --- /dev/null +++ b/client/src/App.scss @@ -0,0 +1,723 @@ +// ============================================ +// DESIGN SYSTEM - FOOD TRACER +// Vychází ze systému Luncheru, aby obě aplikace působily jako jedna rodina. +// ============================================ + +:root, [data-bs-theme="light"] { + // Primary colors + --ft-primary: #16a34a; + --ft-primary-hover: #15803d; + --ft-primary-light: #dcfce7; + + // Background colors + --ft-bg: #f9fafb; + --ft-bg-card: #ffffff; + --ft-bg-hover: #f3f4f6; + + // Text colors + --ft-text: #111827; + --ft-text-secondary: #4b5563; + --ft-text-muted: #9ca3af; + + // Border colors + --ft-border: #e5e7eb; + --ft-border-light: #f3f4f6; + + // Navbar - Dark + --ft-navbar-bg: #111827; + --ft-navbar-text: #f9fafb; + + // Action colors + --ft-success: #16a34a; + --ft-warning: #d97706; + --ft-danger: #dc2626; + --ft-info: #2563eb; + + // Shadows + --ft-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --ft-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --ft-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + + // Border radius + --ft-radius-sm: 6px; + --ft-radius: 10px; + --ft-radius-lg: 12px; + --ft-radius-xl: 16px; + + // Transitions + --ft-transition: all 0.2s ease; + --ft-transition-slow: all 0.3s ease; +} + +[data-bs-theme="dark"] { + --ft-primary: #1db954; + --ft-primary-hover: #1ed760; + --ft-primary-light: #052e16; + + --ft-bg: #121212; + --ft-bg-card: #181818; + --ft-bg-hover: #282828; + + --ft-text: #ffffff; + --ft-text-secondary: #b3b3b3; + --ft-text-muted: #6b7280; + + --ft-border: #282828; + --ft-border-light: #1f1f1f; + + --ft-navbar-bg: #000000; + --ft-navbar-text: #ffffff; + + --ft-success: #1db954; + --ft-warning: #fbbf24; + --ft-danger: #f87171; + --ft-info: #60a5fa; + + --ft-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.5); + --ft-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.5), 0 2px 4px -2px rgb(0 0 0 / 0.4); + --ft-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.5), 0 4px 6px -4px rgb(0 0 0 / 0.4); +} + +// ============================================ +// BASE +// ============================================ + +html, body, #root { + height: 100%; + margin: 0; +} + +html { + overflow-x: hidden; +} + +body { + background-color: var(--ft-bg); + color: var(--ft-text); + transition: var(--ft-transition-slow); +} + +.app-container { + min-height: 100vh; + display: flex; + flex-direction: column; + background-color: var(--ft-bg); +} + +.wrapper { + padding: 24px; + flex: 1; + max-width: 1200px; + margin: 0 auto; + width: 100%; + + @media (max-width: 768px) { + padding: 16px; + } +} + +// ============================================ +// NAVBAR +// ============================================ + +.navbar { + background: var(--ft-navbar-bg) !important; + padding: 12px 24px; + box-shadow: none; + border: none; + + .navbar-brand { + color: var(--ft-navbar-text) !important; + font-weight: 700; + font-size: 1.25rem; + letter-spacing: -0.02em; + + &::before { + content: '🍽'; + margin-right: 8px; + } + } + + .nav { + margin-left: auto; + align-items: center; + gap: 4px; + } + + .nav-link, + .dropdown-toggle { + color: var(--ft-navbar-text) !important; + font-weight: 500; + } + + .nav-pill { + color: var(--ft-navbar-text); + background: transparent; + border: none; + border-radius: var(--ft-radius-sm); + padding: 6px 14px; + font-weight: 500; + font-size: 0.9rem; + transition: var(--ft-transition); + + &:hover { + background: rgb(255 255 255 / 0.1); + } + + &.active { + background: var(--ft-primary); + color: #fff; + } + } + + .theme-toggle { + background: transparent; + border: none; + color: var(--ft-navbar-text); + font-size: 1rem; + padding: 6px 10px; + border-radius: var(--ft-radius-sm); + transition: var(--ft-transition); + + &:hover { + background: rgb(255 255 255 / 0.1); + } + } + + .dropdown-menu { + background: var(--ft-bg-card); + border: 1px solid var(--ft-border); + border-radius: var(--ft-radius); + box-shadow: var(--ft-shadow-lg); + padding: 6px; + + .dropdown-item { + color: var(--ft-text); + border-radius: var(--ft-radius-sm); + padding: 8px 12px; + font-size: 0.9rem; + + &:hover, &:focus { + background: var(--ft-bg-hover); + color: var(--ft-text); + } + } + + .dropdown-divider { + border-color: var(--ft-border); + } + } +} + +// ============================================ +// CARDS & PANELS +// ============================================ + +.card-panel { + background: var(--ft-bg-card); + border: 1px solid var(--ft-border); + border-radius: var(--ft-radius-lg); + box-shadow: var(--ft-shadow-sm); + padding: 20px; +} + +.page-header { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 20px; + + h1 { + font-size: 1.6rem; + font-weight: 700; + letter-spacing: -0.02em; + margin: 0; + color: var(--ft-text); + } + + .page-subtitle { + margin: 4px 0 0; + color: var(--ft-text-secondary); + font-size: 0.9rem; + } +} + +// ============================================ +// SUMMARY TILES +// ============================================ + +.summary-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 12px; + margin-bottom: 20px; +} + +.summary-tile { + background: var(--ft-bg-card); + border: 1px solid var(--ft-border); + border-radius: var(--ft-radius-lg); + box-shadow: var(--ft-shadow-sm); + padding: 16px 18px; + + .summary-label { + display: flex; + align-items: center; + gap: 8px; + color: var(--ft-text-secondary); + font-size: 0.8rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + margin-bottom: 6px; + } + + .summary-value { + font-size: 1.6rem; + font-weight: 700; + letter-spacing: -0.02em; + color: var(--ft-text); + line-height: 1.2; + } + + .summary-hint { + margin-top: 2px; + color: var(--ft-text-muted); + font-size: 0.8rem; + } + + &.accent .summary-value { + color: var(--ft-primary); + } +} + +// ============================================ +// FORMS +// ============================================ + +.form-control, +.form-select { + background: var(--ft-bg-card); + border: 1px solid var(--ft-border); + border-radius: var(--ft-radius-sm); + color: var(--ft-text); + transition: var(--ft-transition); + + &:focus { + background: var(--ft-bg-card); + color: var(--ft-text); + border-color: var(--ft-primary); + box-shadow: 0 0 0 3px var(--ft-primary-light); + } + + &::placeholder { + color: var(--ft-text-muted); + } +} + +.form-label { + color: var(--ft-text-secondary); + font-size: 0.85rem; + font-weight: 600; + margin-bottom: 4px; +} + +.form-text { + color: var(--ft-text-muted); + font-size: 0.8rem; +} + +// ============================================ +// BUTTONS +// ============================================ + +.btn { + border-radius: var(--ft-radius-sm); + font-weight: 600; + transition: var(--ft-transition); +} + +.btn-primary { + background: var(--ft-primary); + border-color: var(--ft-primary); + + &:hover, &:focus, &:active { + background: var(--ft-primary-hover) !important; + border-color: var(--ft-primary-hover) !important; + } + + &:disabled { + background: var(--ft-primary); + border-color: var(--ft-primary); + opacity: 0.55; + } +} + +.btn-outline-secondary { + color: var(--ft-text-secondary); + border-color: var(--ft-border); + + &:hover, &:focus, &:active { + background: var(--ft-bg-hover) !important; + border-color: var(--ft-border) !important; + color: var(--ft-text) !important; + } +} + +.btn-icon { + background: transparent; + border: none; + color: var(--ft-text-muted); + padding: 6px 8px; + border-radius: var(--ft-radius-sm); + transition: var(--ft-transition); + + &:hover { + background: var(--ft-bg-hover); + color: var(--ft-text); + } + + &.danger:hover { + color: var(--ft-danger); + } +} + +// ============================================ +// MODALS +// ============================================ + +.modal-content { + background: var(--ft-bg-card); + color: var(--ft-text); + border: 1px solid var(--ft-border); + border-radius: var(--ft-radius-lg); + box-shadow: var(--ft-shadow-lg); +} + +.modal-header, +.modal-footer { + border-color: var(--ft-border); +} + +.modal-title { + font-weight: 700; + font-size: 1.15rem; +} + +.btn-close { + filter: var(--ft-close-filter, none); +} + +[data-bs-theme="dark"] .btn-close { + filter: invert(1) grayscale(100%) brightness(200%); +} + +// ============================================ +// EMPTY STATE +// ============================================ + +.empty-state { + text-align: center; + padding: 40px 20px; + color: var(--ft-text-secondary); + + .empty-icon { + font-size: 2.4rem; + color: var(--ft-text-muted); + margin-bottom: 12px; + } + + .empty-title { + font-size: 1.05rem; + font-weight: 600; + color: var(--ft-text); + margin-bottom: 4px; + } + + .empty-description { + font-size: 0.9rem; + margin: 0; + } +} + +// ============================================ +// BADGES +// ============================================ + +.tag { + display: inline-flex; + align-items: center; + gap: 5px; + background: var(--ft-bg-hover); + color: var(--ft-text-secondary); + border-radius: 999px; + padding: 2px 10px; + font-size: 0.78rem; + font-weight: 600; + white-space: nowrap; +} + +.tag-import { + background: var(--ft-primary-light); + color: var(--ft-primary); +} + +// ============================================ +// FORMULÁŘ JÍDLA — gramáž a kalorie +// ============================================ + +.meal-fieldset { + border: 1px solid var(--ft-border); + border-radius: var(--ft-radius); + padding: 12px 16px 16px; + + legend { + float: none; + width: auto; + padding: 0 6px; + margin: 0 0 0 -6px; + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--ft-text-muted); + } +} + +// Dopočtená hodnota se needituje, tak vypadá jako text, ne jako pole +.derived-value { + display: flex; + align-items: center; + min-height: 38px; + padding: 0 2px; + font-size: 1.1rem; + font-weight: 700; + color: var(--ft-primary); +} + +.meal-modal-summary { + display: flex; + gap: 14px; + margin-right: auto; + color: var(--ft-text-secondary); + font-size: 0.88rem; + font-weight: 600; +} + +// ============================================ +// HLEDÁNÍ KALORIÍ +// ============================================ + +.calorie-lookup { + width: 100%; + position: relative; + + .calorie-lookup-hint { + display: block; + margin-top: 6px; + font-size: 0.78rem; + color: var(--ft-text-muted); + } + + .calorie-lookup-results { + position: absolute; + z-index: 5; + top: calc(100% + 6px); + right: 0; + width: min(380px, 90vw); + background: var(--ft-bg-card); + border: 1px solid var(--ft-border); + border-radius: var(--ft-radius); + box-shadow: var(--ft-shadow-lg); + padding: 6px; + max-height: 300px; + overflow-y: auto; + } + + .calorie-suggestion { + display: flex; + align-items: baseline; + gap: 8px; + width: 100%; + text-align: left; + background: transparent; + border: none; + border-radius: var(--ft-radius-sm); + padding: 7px 10px; + font-size: 0.85rem; + color: var(--ft-text); + transition: var(--ft-transition); + + &:hover { + background: var(--ft-bg-hover); + } + } + + .calorie-suggestion-icon { + color: var(--ft-primary); + font-size: 0.75rem; + flex: 0 0 auto; + } + + .calorie-suggestion-name { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .calorie-suggestion-brand { + color: var(--ft-text-muted); + font-size: 0.76rem; + flex: 0 0 auto; + } + + .calorie-suggestion-value { + font-weight: 700; + white-space: nowrap; + flex: 0 0 auto; + } + + .calorie-lookup-external { + display: block; + padding: 8px 10px; + margin-top: 4px; + border-top: 1px solid var(--ft-border-light); + font-size: 0.8rem; + color: var(--ft-primary); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } +} + +// ============================================ +// NASTAVENÍ — sazby za 100 g +// ============================================ + +.settings-section-title { + font-size: 0.8rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--ft-text-secondary); + margin: 0 0 6px; +} + +.rate-row { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 0; + border-bottom: 1px solid var(--ft-border-light); + + .rate-source { + flex: 1; + font-weight: 600; + } + + .rate-value { + color: var(--ft-text-secondary); + font-size: 0.87rem; + white-space: nowrap; + } +} + +.rate-form { + display: flex; + gap: 8px; + margin-top: 14px; + + .rate-form-price { + width: 130px; + flex: 0 0 auto; + } +} + +// ============================================ +// ŠABLONY TRÉNINKŮ +// ============================================ + +.workout-row { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 0; + border-bottom: 1px solid var(--ft-border-light); + + .workout-info { + flex: 1; + min-width: 0; + } + + .workout-name { + font-weight: 600; + color: var(--ft-text); + } + + .workout-items { + font-size: 0.8rem; + color: var(--ft-text-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .workout-calories { + color: var(--ft-warning); + font-weight: 700; + font-size: 0.85rem; + white-space: nowrap; + } +} + +.workout-item-row { + display: flex; + gap: 8px; + margin-bottom: 8px; + + .workout-item-quantity { + width: 90px; + flex: 0 0 auto; + } + + .workout-item-unit { + width: 130px; + flex: 0 0 auto; + } + + .workout-item-rate { + width: 110px; + flex: 0 0 auto; + } + + @media (max-width: 640px) { + flex-wrap: wrap; + + .workout-item-quantity, + .workout-item-unit, + .workout-item-rate { + width: auto; + flex: 1; + } + } +} + +.workout-form-footer { + display: flex; + align-items: center; + gap: 8px; + margin-top: 12px; + + .workout-draft-calories { + margin-left: auto; + color: var(--ft-warning); + font-weight: 600; + font-size: 0.85rem; + } +} diff --git a/client/src/AppRoutes.tsx b/client/src/AppRoutes.tsx new file mode 100644 index 0000000..9c31b47 --- /dev/null +++ b/client/src/AppRoutes.tsx @@ -0,0 +1,30 @@ +import { Routes, Route, Navigate } from "react-router-dom"; +import { ToastContainer } from "react-toastify"; +import { useAuth } from "./context/auth"; +import Header from "./components/Header"; +import Login from "./Login"; +import DayPage from "./pages/DayPage"; +import StatsPage from "./pages/StatsPage"; +import { DEN_URL, STATISTIKY_URL } from "./routes"; + +export default function AppRoutes() { + const auth = useAuth(); + + if (!auth?.login) { + return ; + } + + return ( +
+
+
+ + } /> + } /> + } /> + +
+ +
+ ); +} diff --git a/client/src/Login.css b/client/src/Login.css new file mode 100644 index 0000000..d083a60 --- /dev/null +++ b/client/src/Login.css @@ -0,0 +1,69 @@ +.login-page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + background: var(--ft-bg); +} + +.login-card { + width: 100%; + max-width: 420px; + background: var(--ft-bg-card); + border: 1px solid var(--ft-border); + border-radius: var(--ft-radius-xl); + box-shadow: var(--ft-shadow-lg); + padding: 32px; +} + +.login-logo { + margin: 0; + font-size: 1.75rem; + font-weight: 700; + letter-spacing: -0.03em; + color: var(--ft-text); +} + +.login-subtitle { + margin: 6px 0 24px; + color: var(--ft-text-secondary); + font-size: 0.92rem; +} + +.login-form { + display: flex; + flex-direction: column; + gap: 16px; +} + +.login-form label { + display: block; + margin-bottom: 6px; + font-size: 0.85rem; + font-weight: 600; + color: var(--ft-text-secondary); +} + +.login-form input { + width: 100%; + padding: 10px 12px; + border: 1px solid var(--ft-border); + border-radius: var(--ft-radius-sm); + background: var(--ft-bg); + color: var(--ft-text); + font-size: 0.95rem; + transition: var(--ft-transition); +} + +.login-form input:focus { + outline: none; + border-color: var(--ft-primary); + box-shadow: 0 0 0 3px var(--ft-primary-light); +} + +.login-form .hint { + margin: 8px 0 0; + font-size: 0.8rem; + color: var(--ft-text-muted); +} diff --git a/client/src/Login.tsx b/client/src/Login.tsx new file mode 100644 index 0000000..76ef356 --- /dev/null +++ b/client/src/Login.tsx @@ -0,0 +1,63 @@ +import { useCallback, useEffect, useRef } from 'react'; +import { Button } from 'react-bootstrap'; +import { useAuth } from './context/auth'; +import { login } from '../../types'; +import './Login.css'; + +/** + * Formulář pro prvotní zadání přihlašovacího jména. + */ +export default function Login() { + const auth = useAuth(); + const loginRef = useRef(null); + + useEffect(() => { + if (auth && !auth.login) { + // Vyzkoušíme přihlášení "naprázdno" — pokud projde, přihlásily nás trusted headers + login().then(response => { + if (response.data) { + auth.setToken(response.data as unknown as string); + } + }).catch(() => { /* nepřihlášeno, uživatel zadá jméno ručně */ }); + } + }, [auth]); + + const doLogin = useCallback(async () => { + const value = loginRef.current?.value ?? ''; + if (!value.replaceAll(/\s/g, '').length) return; + const response = await login({ body: { login: value } }); + if (response.data) { + auth?.setToken(response.data as unknown as string); + } + }, [auth]); + + return ( +
+
+

🍽 Food Tracer

+

Přehled o tom, co jste snědli a kolik vás to stálo

+
+
+ + { + if (event.key === 'Enter') { + doLogin(); + } + }} + /> +

+ Zadejte jméno nebo přezdívku, pod kterou se vaše záznamy uloží. + Pokud používáte Luncher, hodí se stejné jméno jako tam. +

+
+ +
+
+
+ ); +} diff --git a/client/src/Utils.tsx b/client/src/Utils.tsx new file mode 100644 index 0000000..047366f --- /dev/null +++ b/client/src/Utils.tsx @@ -0,0 +1,195 @@ +const TOKEN_KEY = "token"; + +/** Uloží token do local storage prohlížeče. */ +export const storeToken = (token: string) => { + localStorage.setItem(TOKEN_KEY, token); +} + +/** Vrátí token z local storage, pokud tam je. */ +export const getToken = (): string | undefined => { + return localStorage.getItem(TOKEN_KEY) ?? undefined; +} + +/** Odstraní token z local storage, pokud tam je. */ +export const deleteToken = () => { + localStorage.removeItem(TOKEN_KEY); +} + +/** Vrátí datum v ISO formátu YYYY-MM-DD v lokální časové zóně. */ +export function formatDate(date: Date): string { + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const year = String(date.getFullYear()); + return `${year}-${month}-${day}`; +} + +/** Vrátí měsíc data ve formátu YYYY-MM (hodnota pro ). */ +export function formatMonth(date: Date): string { + return formatDate(date).substring(0, 7); +} + +/** Převede datum ve formátu YYYY-MM-DD na DD.MM.YYYY */ +export function formatDateString(date: string): string { + const [year, month, day] = date.split('-'); + return `${day}.${month}.${year}`; +} + +/** Převede řetězec YYYY-MM-DD na Date v lokální časové zóně. */ +export function parseIsoDate(date: string): Date { + const [year, month, day] = date.split('-').map(Number); + return new Date(year, month - 1, day); +} + +/** Posune datum ve formátu YYYY-MM-DD o zadaný počet dní. */ +export function shiftDate(date: string, days: number): string { + const shifted = parseIsoDate(date); + shifted.setDate(shifted.getDate() + days); + return formatDate(shifted); +} + +/** Vrátí první den měsíce (YYYY-MM) ve formátu YYYY-MM-DD. */ +export function getMonthStart(month: string): string { + return `${month}-01`; +} + +/** Vrátí poslední den měsíce (YYYY-MM) ve formátu YYYY-MM-DD. */ +export function getMonthEnd(month: string): string { + const [year, monthNumber] = month.split('-').map(Number); + // Nultý den následujícího měsíce = poslední den tohoto měsíce + return formatDate(new Date(year, monthNumber, 0)); +} + +const DAY_OF_WEEK_FORMAT = new Intl.DateTimeFormat('cs-CZ', { weekday: 'long' }); + +const HUMAN_DATE_FORMAT = new Intl.DateTimeFormat('cs-CZ', { day: 'numeric', month: 'long', year: 'numeric' }); + +const MONTH_FORMAT = new Intl.DateTimeFormat('cs-CZ', { month: 'long', year: 'numeric' }); + +/** Vrátí název dne v týdnu pro datum ve formátu YYYY-MM-DD. */ +export function getDayOfWeek(date: string): string { + return DAY_OF_WEEK_FORMAT.format(parseIsoDate(date)); +} + +/** Vrátí čitelný zápis data, např. "3. března 2025". */ +export function getHumanDate(date: string): string { + return HUMAN_DATE_FORMAT.format(parseIsoDate(date)); +} + +/** Vrátí čitelný zápis měsíce (YYYY-MM), např. "březen 2025". */ +export function getHumanMonth(month: string): string { + return MONTH_FORMAT.format(parseIsoDate(`${month}-01`)); +} + +const PRICE_FORMAT = new Intl.NumberFormat('cs-CZ', { + style: 'currency', + currency: 'CZK', + maximumFractionDigits: 2, +}); + +/** + * Naformátuje cenu z haléřů na koruny, např. 15900 → "159,00 Kč". + * Nevyplněnou cenu zobrazí jako pomlčku. + */ +export function formatPrice(halere?: number): string { + if (halere == null) return '—'; + return PRICE_FORMAT.format(halere / 100); +} + +/** Naformátuje cenu z haléřů bez desetinných míst — pro souhrny a osy grafů. */ +export function formatPriceShort(halere: number): string { + return `${Math.round(halere / 100).toLocaleString('cs-CZ')} Kč`; +} + +/** + * Převede uživatelem zadanou cenu v korunách na haléře. + * Přijímá desetinnou čárku i tečku. Prázdný vstup vrací undefined. + */ +export function parsePriceInput(value: string): number | undefined { + const trimmed = value.trim().replace(',', '.'); + if (!trimmed.length) return undefined; + const parsed = Number(trimmed); + return Number.isFinite(parsed) && parsed >= 0 ? Math.round(parsed * 100) : undefined; +} + +/** Převede haléře na text pro editační pole (koruny s desetinnou čárkou). */ +export function formatPriceInput(halere?: number): string { + if (halere == null) return ''; + return (halere / 100).toFixed(2).replace('.', ','); +} + +/** Naformátuje počet kalorií, např. 850 → "850 kcal". */ +export function formatCalories(kcal?: number): string { + if (!kcal) return '—'; + return `${kcal.toLocaleString('cs-CZ')} kcal`; +} + +/** Načte soubor jako Base64 řetězec (bez data: prefixu). */ +export function readFileAsBase64(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const result = String(reader.result); + // FileReader vrací "data:;base64,", server chce jen data + resolve(result.substring(result.indexOf(',') + 1)); + }; + reader.onerror = () => reject(new Error('Soubor se nepodařilo načíst')); + reader.readAsDataURL(file); + }); +} + +/** + * Dopočte gramáž a kalorie z jednotkových hodnot. + * + * Musí dávat stejné výsledky jako `deriveAmounts` na serveru — klient jen ukazuje + * živý náhled toho, co server uloží. + * + * @param price cena v haléřích + * @param pricePer100g cena za 100 g v haléřích + * @param weight ručně zadaná gramáž v gramech, má přednost před dopočtem + * @param caloriesPer100g energie na 100 g v kcal + */ +export function deriveAmounts( + price?: number, + pricePer100g?: number, + weight?: number, + caloriesPer100g?: number, +): { weight?: number, calories?: number } { + const resolvedWeight = weight ?? ( + price != null && pricePer100g ? Math.round(price / pricePer100g * 100) : undefined + ); + const calories = resolvedWeight != null && caloriesPer100g + ? Math.round(resolvedWeight / 100 * caloriesPer100g) + : undefined; + return { weight: resolvedWeight, calories }; +} + +/** Naformátuje gramáž, např. 400 → "400 g". */ +export function formatWeight(grams?: number): string { + if (grams == null) return '—'; + return `${grams.toLocaleString('cs-CZ')} g`; +} + +/** Převede celé číslo z pole na number, prázdné pole na undefined. */ +export function parseIntInput(value: string): number | undefined { + const trimmed = value.trim(); + if (!trimmed.length) return undefined; + const parsed = Number(trimmed); + return Number.isFinite(parsed) && parsed >= 0 ? Math.round(parsed) : undefined; +} + +/** Naformátuje množství aktivity i s jednotkou, např. "10 000 kroků". */ +export function formatQuantity(quantity: number, unitShort: string): string { + return `${quantity.toLocaleString('cs-CZ')} ${unitShort}`; +} + +/** Dopočte spálené kalorie z množství a sazby na 100 jednotek. */ +export function deriveActivityCalories(quantity?: number, caloriesPer100Units?: number): number | undefined { + if (quantity == null || !caloriesPer100Units) return undefined; + return Math.round(quantity / 100 * caloriesPer100Units); +} + +/** Naformátuje bilanci se znaménkem, např. "−320 kcal" nebo "+150 kcal". */ +export function formatBalance(kcal: number): string { + const sign = kcal < 0 ? '−' : '+'; + return `${sign}${Math.abs(kcal).toLocaleString('cs-CZ')} kcal`; +} diff --git a/client/src/components/CalorieLookup.tsx b/client/src/components/CalorieLookup.tsx new file mode 100644 index 0000000..017993e --- /dev/null +++ b/client/src/components/CalorieLookup.tsx @@ -0,0 +1,111 @@ +import { useState } from "react"; +import { Button, Spinner } from "react-bootstrap"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faMagnifyingGlass, faArrowUpRightFromSquare, faBookmark } from "@fortawesome/free-solid-svg-icons"; +import { CalorieSearchResult, searchCalories } from "../../../types"; + +type Props = { + /** Název jídla, ke kterému se energie hledá. */ + name: string, + /** Zavolá se po výběru návrhu s energií na 100 g v kcal. */ + onPick: (caloriesPer100g: number) => void, +}; + +/** Návrhy pocházející z vlastní knihovny se odlišují ikonou a popiskem. */ +const LIBRARY_ORIGIN = 'library'; + +/** + * Vyhledání energetické hodnoty k názvu jídla. + * + * Návrhy chodí z vlastní knihovny dřívějších zadání a od externího poskytovatele. + * Když je poskytovatel nedostupný (což se stává), zůstane aspoň knihovna a odkaz + * na KalorickéTabulky.cz, odkud si hodnotu uživatel opíše ručně — automaticky se + * odtud nic nestahuje, veřejné API nemají. + */ +export default function CalorieLookup({ name, onPick }: Readonly) { + const [result, setResult] = useState(); + const [searching, setSearching] = useState(false); + const [copied, setCopied] = useState(false); + + const canSearch = name.trim().length > 0 && !searching; + + /** + * Zkopíruje název jídla do schránky při otevření Kalorických tabulek. + * Jejich vyhledávání nejde předvyplnit z URL, takže aspoň stačí vložit. + */ + const copyName = () => { + navigator.clipboard?.writeText(name.trim()) + .then(() => setCopied(true)) + .catch(() => { /* schránka není dostupná, odkaz se stejně otevře */ }); + }; + + const search = async () => { + if (!canSearch) return; + setSearching(true); + const response = await searchCalories({ query: { q: name.trim() } }); + setResult(response.data); + setCopied(false); + setSearching(false); + }; + + return ( +
+ + + {!canSearch && !name.trim().length && ( + nejdřív vyplňte název jídla + )} + + {result && !searching && ( +
+ {result.suggestions.map(suggestion => ( + + ))} + + {!result.suggestions.length && ( +
+ Nic se nenašlo{!result.providerAvailable && ' a externí databáze je nedostupná'}. +
+ )} + + {result.providerAvailable === false && !!result.suggestions.length && ( +
+ Externí databáze je nedostupná, návrhy jsou jen z vašich dřívějších zadání. +
+ )} + + + {copied ? 'Název zkopírován, vložte ho do jejich hledání' : 'Otevřít KalorickéTabulky.cz'} + {' '} + +
+ )} +
+ ); +} diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx new file mode 100644 index 0000000..52b2e6c --- /dev/null +++ b/client/src/components/Header.tsx @@ -0,0 +1,68 @@ +import { useState } from "react"; +import { Navbar, Nav, NavDropdown } from "react-bootstrap"; +import { useLocation, useNavigate } from "react-router"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faSun, faMoon, faGear } from "@fortawesome/free-solid-svg-icons"; +import { useAuth } from "../context/auth"; +import { useSettings, ThemePreference } from "../context/settings"; +import { DEN_URL, STATISTIKY_URL } from "../routes"; +import SettingsModal from "./modals/SettingsModal"; + +/** Položky hlavní navigace. */ +const NAV_ITEMS = [ + { url: DEN_URL, label: 'Můj den' }, + { url: STATISTIKY_URL, label: 'Statistiky' }, +]; + +export default function Header() { + const [settingsOpen, setSettingsOpen] = useState(false); + const auth = useAuth(); + const settings = useSettings(); + const navigate = useNavigate(); + const location = useLocation(); + + const effectiveDark = settings?.effectiveDark ?? false; + + const toggleTheme = () => { + const next: ThemePreference = effectiveDark ? 'light' : 'dark'; + settings?.setThemePreference(next); + }; + + return ( + + Food Tracer + + + + + setSettingsOpen(false)} /> + + ); +} diff --git a/client/src/components/modals/ActivityModal.tsx b/client/src/components/modals/ActivityModal.tsx new file mode 100644 index 0000000..2fe8220 --- /dev/null +++ b/client/src/components/modals/ActivityModal.tsx @@ -0,0 +1,208 @@ +import { useEffect, useState } from "react"; +import { Button, Form, Modal } from "react-bootstrap"; +import { ActivityEntry, ActivityUnit, DayOverview, addActivity, updateActivity } from "../../../../types"; +import { ACTIVITY_PRESETS, ACTIVITY_UNITS_IN_ORDER, ACTIVITY_UNIT_NAMES } from "../../enums"; +import { deriveActivityCalories, formatCalories, parseIntInput } from "../../Utils"; + +type Props = { + isOpen: boolean, + /** Den, do kterého se zakládá nová aktivita. */ + date: string, + /** Upravovaný záznam. Pokud chybí, jde o založení nového. */ + entry?: ActivityEntry, + onClose: () => void, + onSaved: (day: DayOverview) => void, +}; + +/** Prázdný formulář pro novou aktivitu v daném dni. */ +function emptyForm(date: string) { + return { + date, + name: '', + unit: ActivityUnit.KROKY as ActivityUnit, + quantity: '', + caloriesPer100Units: '', + calories: '', + note: '', + }; +} + +/** + * Dialog pro přidání nebo úpravu pohybové aktivity. + * + * Kalorie se dopočítají z množství a spotřeby na 100 jednotek — sto proto, že + * u kroků by sazba na jeden krok byla zlomek. Výběr známé aktivity předvyplní + * jednotku i orientační sazbu. + */ +export default function ActivityModal({ isOpen, date, entry, onClose, onSaved }: Readonly) { + const [form, setForm] = useState(() => emptyForm(date)); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (!isOpen) return; + setForm(entry + ? { + date: entry.date, + name: entry.name, + unit: entry.unit, + quantity: String(entry.quantity), + caloriesPer100Units: entry.caloriesPer100Units != null ? String(entry.caloriesPer100Units) : '', + calories: entry.calories != null ? String(entry.calories) : '', + note: entry.note ?? '', + } + : emptyForm(date)); + }, [isOpen, entry, date]); + + const update = (changes: Partial) => setForm(current => ({ ...current, ...changes })); + + /** Výběr z nabídky předvyplní jednotku i orientační sazbu. */ + const applyPreset = (name: string) => { + const preset = ACTIVITY_PRESETS.find(item => item.name === name); + if (!preset) { + update({ name }); + return; + } + update({ + name: preset.name, + unit: preset.unit, + caloriesPer100Units: String(preset.caloriesPer100Units), + }); + }; + + const quantity = parseIntInput(form.quantity); + const caloriesPer100Units = parseIntInput(form.caloriesPer100Units); + const derivedCalories = deriveActivityCalories(quantity, caloriesPer100Units); + const manualCalories = parseIntInput(form.calories); + const effectiveCalories = derivedCalories ?? manualCalories; + + const canSave = form.name.trim().length > 0 && quantity != null && !saving; + + const save = async () => { + if (!canSave) return; + setSaving(true); + const body = { + date: form.date, + name: form.name.trim(), + unit: form.unit, + quantity, + caloriesPer100Units, + // Server si kalorie dopočte sám, ručně zadané použije jen jako zálohu + calories: derivedCalories != null ? undefined : manualCalories, + note: form.note.trim() || undefined, + }; + const response = entry + ? await updateActivity({ body: { id: entry.id, ...body } }) + : await addActivity({ body }); + setSaving(false); + if (response.data) { + onSaved(response.data); + } + }; + + return ( + + + {entry ? 'Úprava aktivity' : 'Nová aktivita'} + + +
{ event.preventDefault(); save(); }}> +
+
+ Aktivita + applyPreset(event.target.value)} + /> + + {ACTIVITY_PRESETS.map(preset => ( + +
+
+ Datum + event.target.value && update({ date: event.target.value })} + /> +
+
+ +
+ Množství a spotřeba +
+
+ Množství + update({ quantity: event.target.value.replace(/\D/g, '') })} + /> +
+
+ Jednotka + update({ unit: event.target.value as ActivityUnit })} + > + {ACTIVITY_UNITS_IN_ORDER.map(unit => ( + + ))} + +
+
+ Kcal na 100 jedn. + update({ caloriesPer100Units: event.target.value.replace(/\D/g, '') })} + /> +
+
+ Spáleno + {derivedCalories != null ? ( +
{formatCalories(derivedCalories)}
+ ) : ( +
+ update({ calories: event.target.value.replace(/\D/g, '') })} + /> + kcal +
+ )} +
+
+
+ + + Poznámka + update({ note: event.target.value })} + /> + +
+
+ +
+ {effectiveCalories != null && {formatCalories(effectiveCalories)}} +
+ + +
+
+ ); +} diff --git a/client/src/components/modals/ImportModal.tsx b/client/src/components/modals/ImportModal.tsx new file mode 100644 index 0000000..4c0b238 --- /dev/null +++ b/client/src/components/modals/ImportModal.tsx @@ -0,0 +1,183 @@ +import { useEffect, useState } from "react"; +import { Alert, Button, Form, Modal, Spinner } from "react-bootstrap"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faFileImport, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons"; +import { ImportResult, MealType, importLuncher } from "../../../../types"; +import { MEAL_TYPES_IN_ORDER, MEAL_TYPE_ICONS, MEAL_TYPE_NAMES } from "../../enums"; +import { formatDateString, formatPrice, readFileAsBase64 } from "../../Utils"; + +type Props = { + isOpen: boolean, + onClose: () => void, + /** Zavolá se po dokončení importu, aby se přepočítaly statistiky. */ + onImported: () => void, +}; + +/** Kolik záznamů náhledu se vypíše, než se zbytek shrne do jednoho řádku. */ +const PREVIEW_LIMIT = 8; + +/** + * Dialog pro import měsíčního přehledu ze stránky statistik Luncheru. + * + * Import běží dvoufázově — nejdřív náhled (dryRun), aby uživatel viděl, co se + * založí a kolik řádků už má, a teprve po potvrzení se data uloží. + */ +export default function ImportModal({ isOpen, onClose, onImported }: Readonly) { + const [file, setFile] = useState(); + const [mealType, setMealType] = useState(MealType.OBED); + const [preview, setPreview] = useState(); + const [result, setResult] = useState(); + const [busy, setBusy] = useState(false); + + // Každé otevření začíná načisto, aby nezůstal viset náhled z minula + useEffect(() => { + if (!isOpen) return; + setFile(undefined); + setPreview(undefined); + setResult(undefined); + setBusy(false); + }, [isOpen]); + + /** Odešle soubor na server — buď jako náhled, nebo jako ostrý import. */ + const send = async (dryRun: boolean) => { + if (!file) return; + setBusy(true); + try { + const content = await readFileAsBase64(file); + const response = await importLuncher({ + body: { fileName: file.name, content, defaultMealType: mealType, dryRun }, + }); + if (!response.data) return; + if (dryRun) { + setPreview(response.data); + } else { + setResult(response.data); + setPreview(undefined); + onImported(); + } + } finally { + setBusy(false); + } + }; + + const chooseFile = (chosen?: File) => { + setFile(chosen); + setPreview(undefined); + setResult(undefined); + }; + + return ( + + + + Import z Luncheru + + + +

+ Na stránce statistik v Luncheru si stáhněte přehled za měsíc (XLSX, CSV nebo JSON) + a nahrajte ho sem. Záznamy se založí do dnů, ke kterým patří. Opakovaný import + stejného měsíce data nezduplikuje. +

+ + + Soubor s přehledem + chooseFile((event.target as HTMLInputElement).files?.[0])} + /> + + + + Založit jako + { setMealType(event.target.value as MealType); setPreview(undefined); }} + > + {MEAL_TYPES_IN_ORDER.map(type => ( + + ))} + + + Luncher řeší výběr obědů, proto je předvolený oběd. Jednotlivá jídla si pak + můžete v přehledu dne upravit nebo doplnit o další. + + + + {busy && ( +
+ +
+ )} + + {preview && !busy && ( + + Náhled importu +
+ V souboru je {preview.rowCount} řádků. Založí se {preview.imported} záznamů + {preview.skipped > 0 && <> a {preview.skipped} se přeskočí, protože už je máte}. + {preview.days.length > 0 && ( + <> Dotčené dny: {formatDateString(preview.days[0])} + {preview.days.length > 1 && <> – {formatDateString(preview.days[preview.days.length - 1])}} + {' '}({preview.days.length}). + )} + {' '}Celkem za {formatPrice(preview.totalPrice)}. +
+ {preview.entries.length > 0 && ( +
    + {preview.entries.slice(0, PREVIEW_LIMIT).map(entry => ( +
  • + {formatDateString(entry.date)} + {entry.name} + {entry.source && · {entry.source}} + {formatPrice(entry.price)} +
  • + ))} + {preview.entries.length > PREVIEW_LIMIT && ( +
  • + … a dalších {preview.entries.length - PREVIEW_LIMIT} +
  • + )} +
+ )} +
+ )} + + {result && !busy && ( + + Import dokončen +
+ Založeno {result.imported} záznamů ve {result.days.length} dnech + {result.skipped > 0 && <>, {result.skipped} přeskočeno jako již existující}. + Celkem za {formatPrice(result.totalPrice)}. +
+
+ )} + + {(preview ?? result)?.warnings.map(warning => ( +
+ {warning} +
+ ))} +
+ + + {!result && !preview && ( + + )} + {!result && preview && ( + + )} + +
+ ); +} diff --git a/client/src/components/modals/MealModal.tsx b/client/src/components/modals/MealModal.tsx new file mode 100644 index 0000000..eef073a --- /dev/null +++ b/client/src/components/modals/MealModal.tsx @@ -0,0 +1,309 @@ +import { useEffect, useMemo, useState } from "react"; +import { Button, Form, Modal } from "react-bootstrap"; +import { DayRecord, MealEntry, MealType, UserSettings, addMeal, getSettings, updateMeal } from "../../../../types"; +import { MEAL_TYPES_IN_ORDER, MEAL_TYPE_ICONS, MEAL_TYPE_NAMES } from "../../enums"; +import { + deriveAmounts, formatCalories, formatPriceInput, formatWeight, parseIntInput, parsePriceInput, +} from "../../Utils"; +import CalorieLookup from "../CalorieLookup"; + +type Props = { + isOpen: boolean, + /** Den, do kterého se zakládá nové jídlo. */ + date: string, + /** Upravovaný záznam. Pokud chybí, jde o založení nového. */ + entry?: MealEntry, + onClose: () => void, + onSaved: (day: DayRecord) => void, +}; + +/** Prázdný formulář pro nové jídlo v daném dni. */ +function emptyForm(date: string) { + return { + date, + mealType: MealType.OBED as MealType, + name: '', + source: '', + price: '', + pricePer100g: '', + weight: '', + caloriesPer100g: '', + calories: '', + note: '', + }; +} + +/** Znormalizuje název zdroje pro porovnání se sazbami z nastavení. */ +function normalizeSource(source: string): string { + return source.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim(); +} + +/** + * Dialog pro přidání nebo úpravu jídla. + * + * Kromě ceny umí i gramáž — u podniků účtujících podle váhy (TechTower, 44 Kč/100 g) + * se dopočte z ceny a sazby, ale dá se přepsat. Z gramáže a energie na 100 g se + * pak dopočtou kalorie. Každá položka má vlastní sazbu, takže hlavní jídlo a salát + * s jiným cenováním můžou být ve stejném dni vedle sebe. + */ +export default function MealModal({ isOpen, date, entry, onClose, onSaved }: Readonly) { + const [form, setForm] = useState(() => emptyForm(date)); + const [settings, setSettings] = useState(); + const [saving, setSaving] = useState(false); + + // Při každém otevření se formulář naplní dle toho, co se upravuje + useEffect(() => { + if (!isOpen) return; + setForm(entry + ? { + date: entry.date, + mealType: entry.mealType, + name: entry.name, + source: entry.source ?? '', + price: formatPriceInput(entry.price), + pricePer100g: formatPriceInput(entry.pricePer100g), + weight: entry.weight != null ? String(entry.weight) : '', + caloriesPer100g: entry.caloriesPer100g != null ? String(entry.caloriesPer100g) : '', + calories: entry.calories != null ? String(entry.calories) : '', + note: entry.note ?? '', + } + : emptyForm(date)); + }, [isOpen, entry, date]); + + // Sazby podniků se načtou jednou při otevření a slouží k předvyplnění Kč/100 g + useEffect(() => { + if (!isOpen || settings) return; + getSettings().then(response => setSettings(response.data)); + }, [isOpen, settings]); + + const update = (changes: Partial) => setForm(current => ({ ...current, ...changes })); + + /** Sazba nastavená pro právě zadaný zdroj, pokud nějaká je. */ + const rateForSource = useMemo(() => { + const key = normalizeSource(form.source); + if (!key.length) return undefined; + return settings?.sourceRates.find(rate => normalizeSource(rate.source) === key)?.pricePer100g; + }, [settings, form.source]); + + /** + * Doplní sazbu podle zdroje, jakmile ji uživatel vybere a pole je prázdné. + * Ručně zadanou sazbu nepřepisujeme — u salátu bývá jiná než u hlavního jídla. + */ + useEffect(() => { + if (rateForSource && !form.pricePer100g.trim().length) { + setForm(current => ({ ...current, pricePer100g: formatPriceInput(rateForSource) })); + } + }, [rateForSource, form.pricePer100g]); + + const price = parsePriceInput(form.price); + const pricePer100g = parsePriceInput(form.pricePer100g); + const manualWeight = parseIntInput(form.weight); + const caloriesPer100g = parseIntInput(form.caloriesPer100g); + const derived = deriveAmounts(price, pricePer100g, manualWeight, caloriesPer100g); + + /** Kalorie zadané ručně se použijí jen tehdy, když nejdou dopočítat. */ + const manualCalories = parseIntInput(form.calories); + const effectiveCalories = derived.calories ?? manualCalories; + + const canSave = form.name.trim().length > 0 && !saving; + + const save = async () => { + if (!canSave) return; + setSaving(true); + const body = { + date: form.date, + mealType: form.mealType, + name: form.name.trim(), + source: form.source.trim() || undefined, + price, + pricePer100g, + weight: manualWeight, + caloriesPer100g, + // Server si kalorie dopočte sám, ručně poslané použije jen jako zálohu + calories: derived.calories != null ? undefined : manualCalories, + note: form.note.trim() || undefined, + }; + const response = entry + ? await updateMeal({ body: { id: entry.id, ...body } }) + : await addMeal({ body }); + setSaving(false); + if (response.data) { + onSaved(response.data); + } + }; + + return ( + + + {entry ? 'Úprava jídla' : 'Nové jídlo'} + + +
{ event.preventDefault(); save(); }}> + + Jídlo + update({ name: event.target.value })} + /> + + +
+
+ Chod + update({ mealType: event.target.value as MealType })} + > + {MEAL_TYPES_IN_ORDER.map(mealType => ( + + ))} + +
+
+ Datum + event.target.value && update({ date: event.target.value })} + /> +
+
+ Zdroj + update({ source: event.target.value })} + /> + + {settings?.sourceRates.map(rate => ( + +
+
+ +
+ Cena a gramáž +
+
+ Cena +
+ update({ price: event.target.value })} + /> + +
+
+
+ Cena za 100 g +
+ update({ pricePer100g: event.target.value })} + /> + +
+ {rateForSource != null && ( + sazba zdroje {form.source.trim()} + )} +
+
+ Gramáž +
+ update({ weight: event.target.value.replace(/\D/g, '') })} + /> + g +
+ {manualWeight == null && derived.weight != null && ( + dopočteno z ceny + )} + {manualWeight != null && pricePer100g != null && ( + zadáno ručně, cena se nemění + )} +
+
+
+ +
+ Kalorie +
+
+ Energie na 100 g +
+ update({ caloriesPer100g: event.target.value.replace(/\D/g, '') })} + /> + kcal +
+
+
+ Kalorie celkem + {derived.calories != null ? ( +
{formatCalories(derived.calories)}
+ ) : ( +
+ update({ calories: event.target.value.replace(/\D/g, '') })} + /> + kcal +
+ )} + {derived.calories != null && ( + + {formatWeight(derived.weight)} × {caloriesPer100g} kcal/100 g + + )} +
+
+ update({ caloriesPer100g: String(value) })} + /> +
+
+
+ + + Poznámka + update({ note: event.target.value })} + /> + +
+
+ +
+ {derived.weight != null && {formatWeight(derived.weight)}} + {effectiveCalories != null && {formatCalories(effectiveCalories)}} +
+ + +
+
+ ); +} diff --git a/client/src/components/modals/SettingsModal.tsx b/client/src/components/modals/SettingsModal.tsx new file mode 100644 index 0000000..ceb5dbd --- /dev/null +++ b/client/src/components/modals/SettingsModal.tsx @@ -0,0 +1,144 @@ +import { useEffect, useState } from "react"; +import { Button, Form, Modal } from "react-bootstrap"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faTrash, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { UserSettings, getSettings, saveBasalCalories, saveSourceRate } from "../../../../types"; +import { formatPriceInput, parseIntInput, parsePriceInput } from "../../Utils"; + +type Props = { + isOpen: boolean, + onClose: () => void, +}; + +/** + * Nastavení sazeb za 100 g u podniků, které účtují podle váhy. + * + * Sazba slouží jako výchozí hodnota při zadávání jídla — u konkrétní položky + * ji jde přepsat, protože salát bývá účtovaný jinak než hlavní jídlo. + */ +export default function SettingsModal({ isOpen, onClose }: Readonly) { + const [settings, setSettings] = useState(); + const [newSource, setNewSource] = useState(''); + const [newRate, setNewRate] = useState(''); + const [basal, setBasal] = useState(''); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (!isOpen) return; + setNewSource(''); + setNewRate(''); + getSettings().then(response => { + setSettings(response.data); + setBasal(response.data?.basalCalories != null ? String(response.data.basalCalories) : ''); + }); + }, [isOpen]); + + const save = async (source: string, pricePer100g: number | null) => { + setSaving(true); + const response = await saveSourceRate({ body: { source, pricePer100g } }); + setSaving(false); + if (response.data) { + setSettings(response.data); + } + }; + + /** Uloží klidový výdej. Prázdné pole ho odstraní. */ + const saveBasal = async () => { + setSaving(true); + const response = await saveBasalCalories({ body: { basalCalories: parseIntInput(basal) ?? null } }); + setSaving(false); + if (response.data) { + setSettings(response.data); + } + }; + + const addRate = async () => { + const rate = parsePriceInput(newRate); + if (!newSource.trim().length || !rate) return; + await save(newSource.trim(), rate); + setNewSource(''); + setNewRate(''); + }; + + return ( + + + Nastavení + + +

Klidový výdej

+

+ Kolik kcal spálíte za den bez pohybu (bazální metabolismus). Bez něj + porovnává bilance dne jen jídlo proti pohybu a nejde o skutečný deficit. +

+
+
+ setBasal(event.target.value.replace(/\D/g, ''))} + /> + kcal / den +
+ +
+ +

Cena za 100 g

+

+ U podniků, které účtují podle váhy, se z ceny jídla a této sazby dopočítá + gramáž a z ní kalorie. U konkrétního jídla jde sazbu vždy přepsat. +

+ + {settings?.sourceRates.length === 0 && ( +

Zatím nemáte žádnou sazbu.

+ )} + + {settings?.sourceRates.map(rate => ( +
+ {rate.source} + {formatPriceInput(rate.pricePer100g)} Kč / 100 g + +
+ ))} + +
{ event.preventDefault(); addRate(); }} + > + setNewSource(event.target.value)} + /> +
+ setNewRate(event.target.value)} + /> + +
+ + +
+ + + +
+ ); +} diff --git a/client/src/components/modals/WorkoutModal.tsx b/client/src/components/modals/WorkoutModal.tsx new file mode 100644 index 0000000..09a4d33 --- /dev/null +++ b/client/src/components/modals/WorkoutModal.tsx @@ -0,0 +1,257 @@ +import { useEffect, useState } from "react"; +import { Button, Form, Modal } from "react-bootstrap"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faTrash, faPlus, faPlay, faPenToSquare } from "@fortawesome/free-solid-svg-icons"; +import { + ActivityUnit, DayOverview, WorkoutTemplate, WorkoutTemplateItem, + applyWorkoutTemplate, deleteWorkoutTemplate, getWorkoutTemplates, saveWorkoutTemplate, +} from "../../../../types"; +import { ACTIVITY_PRESETS, ACTIVITY_UNITS_IN_ORDER, ACTIVITY_UNIT_NAMES, ACTIVITY_UNIT_SHORT } from "../../enums"; +import { deriveActivityCalories, formatCalories, formatQuantity, parseIntInput } from "../../Utils"; + +type Props = { + isOpen: boolean, + /** Den, do kterého se šablona použije. */ + date: string, + onClose: () => void, + /** Zavolá se po použití šablony s aktualizovaným přehledem dne. */ + onApplied: (day: DayOverview) => void, +}; + +/** Rozpracovaná položka šablony — hodnoty jsou texty, protože se editují v polích. */ +type DraftItem = { + name: string, + unit: ActivityUnit, + quantity: string, + caloriesPer100Units: string, +}; + +function emptyItem(): DraftItem { + return { name: '', unit: ActivityUnit.OPAKOVANI, quantity: '', caloriesPer100Units: '' }; +} + +/** + * Správa šablon tréninků a jejich použití na den. + * + * Šablona je seznam cviků, který se jedním kliknutím založí do dne — opakovaný + * trénink tak není potřeba zadávat cvik po cviku znovu. Založené položky jsou + * pak samostatné aktivity, takže úprava jedné z nich šablonu nemění. + */ +export default function WorkoutModal({ isOpen, date, onClose, onApplied }: Readonly) { + const [templates, setTemplates] = useState([]); + const [editedId, setEditedId] = useState(); + const [name, setName] = useState(''); + const [items, setItems] = useState([emptyItem()]); + const [busy, setBusy] = useState(false); + + const resetForm = () => { + setEditedId(undefined); + setName(''); + setItems([emptyItem()]); + }; + + useEffect(() => { + if (!isOpen) return; + resetForm(); + getWorkoutTemplates().then(response => setTemplates(response.data ?? [])); + }, [isOpen]); + + const updateItem = (index: number, changes: Partial) => + setItems(current => current.map((item, i) => (i === index ? { ...item, ...changes } : item))); + + /** Výběr známého cviku předvyplní jednotku i orientační sazbu. */ + const applyPreset = (index: number, value: string) => { + const preset = ACTIVITY_PRESETS.find(item => item.name === value); + updateItem(index, preset + ? { name: preset.name, unit: preset.unit, caloriesPer100Units: String(preset.caloriesPer100Units) } + : { name: value }); + }; + + /** Převede rozpracované položky na ty, které jdou uložit. */ + const toItems = (): WorkoutTemplateItem[] => items + .filter(item => item.name.trim().length && parseIntInput(item.quantity) != null) + .map(item => ({ + name: item.name.trim(), + unit: item.unit, + quantity: parseIntInput(item.quantity)!, + ...(parseIntInput(item.caloriesPer100Units) != null + ? { caloriesPer100Units: parseIntInput(item.caloriesPer100Units) } + : {}), + })); + + const canSave = name.trim().length > 0 && toItems().length > 0 && !busy; + + const save = async () => { + if (!canSave) return; + setBusy(true); + const response = await saveWorkoutTemplate({ body: { id: editedId, name: name.trim(), items: toItems() } }); + setBusy(false); + if (response.data) { + setTemplates(response.data); + resetForm(); + } + }; + + const remove = async (id: string) => { + setBusy(true); + const response = await deleteWorkoutTemplate({ body: { id } }); + setBusy(false); + if (response.data) { + setTemplates(response.data); + if (editedId === id) resetForm(); + } + }; + + const apply = async (id: string) => { + setBusy(true); + const response = await applyWorkoutTemplate({ body: { id, date } }); + setBusy(false); + if (response.data) { + onApplied(response.data); + } + }; + + /** Načte šablonu do formuláře k úpravě. */ + const edit = (template: WorkoutTemplate) => { + setEditedId(template.id); + setName(template.name); + setItems(template.items.map(item => ({ + name: item.name, + unit: item.unit, + quantity: String(item.quantity), + caloriesPer100Units: item.caloriesPer100Units != null ? String(item.caloriesPer100Units) : '', + }))); + }; + + /** Průběžný odhad kalorií rozpracované šablony. */ + const draftCalories = items.reduce((sum, item) => + sum + (deriveActivityCalories(parseIntInput(item.quantity), parseIntInput(item.caloriesPer100Units)) ?? 0), 0); + + return ( + + + Šablony tréninků + + + {templates.length === 0 && ( +

+ Zatím nemáte žádnou šablonu. Sestavte si ji níže — příště pak stačí jedno kliknutí. +

+ )} + + {templates.map(template => ( +
+
+
{template.name}
+
+ {template.items.map(item => + `${item.name} ${formatQuantity(item.quantity, ACTIVITY_UNIT_SHORT[item.unit])}` + ).join(' · ')} +
+
+ {formatCalories(template.estimatedCalories)} + + + +
+ ))} + +
+ {editedId ? 'Úprava šablony' : 'Nová šablona'} + + + Název + setName(event.target.value)} + /> + + + {items.map((item, index) => ( + // Položky nemají stabilní id, dokud se šablona neuloží — index je tu jediný klíč +
+ applyPreset(index, event.target.value)} + /> + updateItem(index, { quantity: event.target.value.replace(/\D/g, '') })} + /> + updateItem(index, { unit: event.target.value as ActivityUnit })} + > + {ACTIVITY_UNITS_IN_ORDER.map(unit => ( + + ))} + + updateItem(index, { caloriesPer100Units: event.target.value.replace(/\D/g, '') })} + /> + +
+ ))} + + {ACTIVITY_PRESETS.map(preset => ( + + +
+ + {draftCalories > 0 && ( + celkem {formatCalories(draftCalories)} + )} + {editedId && ( + + )} + +
+
+
+ + + +
+ ); +} diff --git a/client/src/context/auth.tsx b/client/src/context/auth.tsx new file mode 100644 index 0000000..b96c3af --- /dev/null +++ b/client/src/context/auth.tsx @@ -0,0 +1,68 @@ +import React, { ReactNode, useContext, useEffect, useState } from "react"; +import { useJwt } from "react-jwt"; +import { deleteToken, getToken, storeToken } from "../Utils"; + +export type AuthContextProps = { + login?: string, + trusted?: boolean, + setToken: (token: string) => void, + logout: () => void, +} + +type ContextProps = { + children: ReactNode +} + +const authContext = React.createContext(null); + +export function ProvideAuth(props: Readonly) { + const auth = useProvideAuth(); + return {props.children} +} + +export const useAuth = () => { + return useContext(authContext); +} + +function useProvideAuth(): AuthContextProps { + const [loginName, setLoginName] = useState(); + const [trusted, setTrusted] = useState(); + const [token, setToken] = useState(getToken()); + const { decodedToken } = useJwt(token ?? ''); + + useEffect(() => { + if (token && token.length > 0) { + storeToken(token); + } else { + deleteToken(); + } + }, [token]); + + useEffect(() => { + if (decodedToken) { + setLoginName((decodedToken as any).login); + setTrusted((decodedToken as any).trusted); + } else { + setLoginName(undefined); + setTrusted(undefined); + } + }, [decodedToken]); + + function logout() { + const isTrusted = (decodedToken as any)?.trusted; + const logoutUrl = (decodedToken as any)?.logoutUrl; + setToken(undefined); + setLoginName(undefined); + setTrusted(undefined); + if (isTrusted && logoutUrl?.length) { + globalThis.location.replace(logoutUrl); + } + } + + return { + login: loginName, + trusted, + setToken, + logout, + } +} diff --git a/client/src/context/settings.tsx b/client/src/context/settings.tsx new file mode 100644 index 0000000..93c41db --- /dev/null +++ b/client/src/context/settings.tsx @@ -0,0 +1,60 @@ +import React, { ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react"; + +/** Volba motivu — buď pevná, nebo řízená nastavením operačního systému. */ +export type ThemePreference = 'light' | 'dark' | 'system'; + +const THEME_KEY = 'themePreference'; + +export type SettingsContextProps = { + themePreference: ThemePreference, + /** Skutečně použitý motiv po vyhodnocení volby 'system'. */ + effectiveDark: boolean, + setThemePreference: (preference: ThemePreference) => void, +}; + +const settingsContext = React.createContext(null); + +export const useSettings = () => useContext(settingsContext); + +/** Vrátí uloženou volbu motivu, nebo 'system', pokud uživatel nic nezvolil. */ +function getStoredPreference(): ThemePreference { + const stored = localStorage.getItem(THEME_KEY); + return stored === 'light' || stored === 'dark' ? stored : 'system'; +} + +export function ProvideSettings(props: Readonly<{ children: ReactNode }>) { + const [themePreference, setStoredPreference] = useState(getStoredPreference); + const [systemDark, setSystemDark] = useState(() => + globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false); + + // Při volbě 'system' musí přepnutí motivu v OS překreslit i běžící aplikaci + useEffect(() => { + const query = globalThis.matchMedia?.('(prefers-color-scheme: dark)'); + if (!query) return; + const listener = (event: MediaQueryListEvent) => setSystemDark(event.matches); + query.addEventListener('change', listener); + return () => query.removeEventListener('change', listener); + }, []); + + const effectiveDark = themePreference === 'system' ? systemDark : themePreference === 'dark'; + + // Bootstrap i vlastní proměnné se řídí atributem na + useEffect(() => { + document.documentElement.setAttribute('data-bs-theme', effectiveDark ? 'dark' : 'light'); + }, [effectiveDark]); + + const setThemePreference = useCallback((preference: ThemePreference) => { + setStoredPreference(preference); + if (preference === 'system') { + localStorage.removeItem(THEME_KEY); + } else { + localStorage.setItem(THEME_KEY, preference); + } + }, []); + + const value = useMemo( + () => ({ themePreference, effectiveDark, setThemePreference }), + [themePreference, effectiveDark, setThemePreference]); + + return {props.children}; +} diff --git a/client/src/enums.ts b/client/src/enums.ts new file mode 100644 index 0000000..7a952a0 --- /dev/null +++ b/client/src/enums.ts @@ -0,0 +1,73 @@ +import { ActivityUnit, MealType } from "../../types"; + +/** Lidsky čitelné názvy typů jídel. */ +export const MEAL_TYPE_NAMES: Record = { + SNIDANE: 'Snídaně', + DOPOLEDNI_SVACINA: 'Dopolední svačina', + OBED: 'Oběd', + ODPOLEDNI_SVACINA: 'Odpolední svačina', + VECERE: 'Večeře', + JINE: 'Jiné', +}; + +/** Emoji doprovázející typ jídla — odlišuje chody v seznamu na první pohled. */ +export const MEAL_TYPE_ICONS: Record = { + SNIDANE: '🥐', + DOPOLEDNI_SVACINA: '🍎', + OBED: '🍲', + ODPOLEDNI_SVACINA: '🥨', + VECERE: '🍽', + JINE: '🍫', +}; + +/** Typy jídel v pořadí, v jakém probíhá den. Určuje i pořadí v seznamech a výběrech. */ +export const MEAL_TYPES_IN_ORDER: MealType[] = [ + MealType.SNIDANE, + MealType.DOPOLEDNI_SVACINA, + MealType.OBED, + MealType.ODPOLEDNI_SVACINA, + MealType.VECERE, + MealType.JINE, +]; + +/** Lidsky čitelné názvy jednotek aktivit. */ +export const ACTIVITY_UNIT_NAMES: Record = { + KROKY: 'kroky', + MINUTY: 'minuty', + KM: 'km', + OPAKOVANI: 'opakování', +}; + +/** Zkratka jednotky za číslo, např. "10 000 kroků". */ +export const ACTIVITY_UNIT_SHORT: Record = { + KROKY: 'kroků', + MINUTY: 'min', + KM: 'km', + OPAKOVANI: '×', +}; + +/** Jednotky v pořadí, v jakém se nabízejí. */ +export const ACTIVITY_UNITS_IN_ORDER: ActivityUnit[] = [ + ActivityUnit.KROKY, + ActivityUnit.MINUTY, + ActivityUnit.KM, + ActivityUnit.OPAKOVANI, +]; + +/** + * Orientační spotřeba běžných aktivit v kcal na 100 jednotek. + * + * Hodnoty jsou hrubý odhad pro člověka kolem 75 kg a slouží jen jako + * předvyplnění — uživatel si je může kdykoli přepsat. + */ +export const ACTIVITY_PRESETS: { name: string, unit: ActivityUnit, caloriesPer100Units: number }[] = [ + { name: 'Chůze', unit: ActivityUnit.KROKY, caloriesPer100Units: 4 }, + { name: 'Svižná chůze', unit: ActivityUnit.MINUTY, caloriesPer100Units: 500 }, + { name: 'Běh', unit: ActivityUnit.MINUTY, caloriesPer100Units: 1000 }, + { name: 'Kolo', unit: ActivityUnit.MINUTY, caloriesPer100Units: 700 }, + { name: 'Plavání', unit: ActivityUnit.MINUTY, caloriesPer100Units: 800 }, + { name: 'Posilovna', unit: ActivityUnit.MINUTY, caloriesPer100Units: 600 }, + { name: 'Kliky', unit: ActivityUnit.OPAKOVANI, caloriesPer100Units: 50 }, + { name: 'Dřepy', unit: ActivityUnit.OPAKOVANI, caloriesPer100Units: 40 }, + { name: 'Shyby', unit: ActivityUnit.OPAKOVANI, caloriesPer100Units: 100 }, +]; diff --git a/client/src/index.css b/client/src/index.css new file mode 100644 index 0000000..b4e5101 --- /dev/null +++ b/client/src/index.css @@ -0,0 +1,35 @@ +html, +body, +#root { + width: 100%; + height: 100%; +} + +body { + margin: 0; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + line-height: 1.5; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} + +html { + scroll-behavior: smooth; +} + +:focus-visible { + outline: 2px solid var(--ft-primary); + outline-offset: 2px; +} + +::selection { + background: var(--ft-primary-light); + color: var(--ft-primary); +} diff --git a/client/src/index.tsx b/client/src/index.tsx new file mode 100644 index 0000000..e0d9622 --- /dev/null +++ b/client/src/index.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router'; +import { toast } from 'react-toastify'; +import { ProvideAuth } from './context/auth'; +import { ProvideSettings } from './context/settings'; +import AppRoutes from './AppRoutes'; +import { client } from '../../types/gen/client.gen'; +import { getToken } from './Utils'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import 'react-toastify/dist/ReactToastify.css'; +import './index.css'; +import './App.scss'; + +client.setConfig({ + auth: () => getToken(), + baseUrl: '/api', // openapi-ts si to neumí převzít z api.yml +}); + +// Interceptor na vyhození toasteru při chybě +client.interceptors.response.use(async response => { + // Login je výjimka — voláme ho "naprázdno", abychom zjistili, zda nás nepřihlásily trusted headers + if (!response.ok && !response.url.includes('/login')) { + const json = await response.clone().json().catch(() => ({})); + toast.error(json.error ?? 'Něco se nepovedlo', { theme: 'colored' }); + } + return response; +}); + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + + + +); diff --git a/client/src/pages/DayPage.scss b/client/src/pages/DayPage.scss new file mode 100644 index 0000000..2cde90e --- /dev/null +++ b/client/src/pages/DayPage.scss @@ -0,0 +1,200 @@ +.day-picker { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; + padding: 12px 16px; + + .day-picker-current { + flex: 1; + min-width: 0; + } + + .day-picker-date { + font-weight: 700; + font-size: 1.05rem; + color: var(--ft-text); + letter-spacing: -0.01em; + } + + .day-picker-weekday { + font-size: 0.82rem; + color: var(--ft-text-secondary); + text-transform: lowercase; + } + + .day-picker-input { + width: auto; + flex: 0 0 auto; + } + + @media (max-width: 576px) { + flex-wrap: wrap; + + .day-picker-input { + order: 10; + width: 100%; + } + } +} + +.meal-group { + margin-bottom: 20px; + + .meal-group-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.85rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--ft-text-secondary); + margin: 0 4px 8px; + } + + .meal-group-icon { + font-size: 1rem; + } + + .meal-group-total { + margin-left: auto; + color: var(--ft-text-muted); + font-weight: 600; + letter-spacing: 0; + text-transform: none; + } +} + +.meal-list { + padding: 0; + overflow: hidden; +} + +.meal-row { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 14px 18px; + border-bottom: 1px solid var(--ft-border-light); + transition: var(--ft-transition); + + &:last-child { + border-bottom: none; + } + + &:hover { + background: var(--ft-bg-hover); + } + + .meal-main { + flex: 1; + min-width: 0; + } + + .meal-name { + font-weight: 600; + color: var(--ft-text); + word-break: break-word; + } + + .meal-meta { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 5px; + } + + .meal-note { + margin-top: 5px; + font-size: 0.83rem; + color: var(--ft-text-muted); + word-break: break-word; + } + + .meal-price { + font-weight: 700; + color: var(--ft-text); + white-space: nowrap; + padding-top: 1px; + } + + .meal-actions { + display: flex; + gap: 2px; + flex: 0 0 auto; + } +} + +.header-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +// ============================================ +// ZÁLOŽKY PŘÍJEM / VÝDEJ +// ============================================ + +.day-tabs { + display: flex; + gap: 4px; + margin-bottom: 16px; + border-bottom: 1px solid var(--ft-border); +} + +.day-tab { + display: flex; + align-items: center; + gap: 8px; + background: transparent; + border: none; + border-bottom: 2px solid transparent; + padding: 10px 18px; + margin-bottom: -1px; + font-weight: 600; + font-size: 0.92rem; + color: var(--ft-text-secondary); + transition: var(--ft-transition); + + &:hover { + color: var(--ft-text); + } + + &.active { + color: var(--ft-primary); + border-bottom-color: var(--ft-primary); + } + + .day-tab-count { + background: var(--ft-bg-hover); + border-radius: 999px; + padding: 1px 8px; + font-size: 0.76rem; + font-weight: 700; + } + + &.active .day-tab-count { + background: var(--ft-primary-light); + color: var(--ft-primary); + } +} + +// ============================================ +// BILANCE +// ============================================ + +.balance-tile { + &.deficit .summary-value { + color: var(--ft-success); + } + + &.surplus .summary-value { + color: var(--ft-warning); + } +} + +// Spálené kalorie se odlišují od ceny, aby v seznamu nesplývaly +.meal-price.burn { + color: var(--ft-warning); +} diff --git a/client/src/pages/DayPage.tsx b/client/src/pages/DayPage.tsx new file mode 100644 index 0000000..687453c --- /dev/null +++ b/client/src/pages/DayPage.tsx @@ -0,0 +1,351 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Button, Spinner } from "react-bootstrap"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faChevronLeft, faChevronRight, faPlus, faPenToSquare, faTrash, + faUtensils, faPersonRunning, faCoins, faFire, faScaleBalanced, faDumbbell, +} from "@fortawesome/free-solid-svg-icons"; +import { ActivityEntry, DayOverview, MealEntry, deleteActivity, deleteMeal, getDayOverview } from "../../../types"; +import { + ACTIVITY_UNIT_SHORT, MEAL_TYPES_IN_ORDER, MEAL_TYPE_ICONS, MEAL_TYPE_NAMES, +} from "../enums"; +import { + formatBalance, formatCalories, formatDate, formatPrice, formatQuantity, formatWeight, + getDayOfWeek, getHumanDate, shiftDate, +} from "../Utils"; +import MealModal from "../components/modals/MealModal"; +import ActivityModal from "../components/modals/ActivityModal"; +import WorkoutModal from "../components/modals/WorkoutModal"; +import "./DayPage.scss"; + +/** Záložky dne — co do těla přišlo a co se vydalo. */ +type Tab = 'prijem' | 'vydej'; + +/** Prázdný přehled dne — použije se, než dorazí data ze serveru. */ +function emptyOverview(date: string): DayOverview { + return { + date, + meals: { date, entries: [], totalPrice: 0, totalCalories: 0 }, + activities: { date, entries: [], totalCalories: 0 }, + energy: { intake: 0, activityBurn: 0, basal: 0, totalBurn: 0, balance: 0, hasBasal: false }, + }; +} + +export default function DayPage() { + const [date, setDate] = useState(() => formatDate(new Date())); + const [day, setDay] = useState(() => emptyOverview(date)); + const [tab, setTab] = useState('prijem'); + const [loading, setLoading] = useState(true); + const [editedMeal, setEditedMeal] = useState(); + const [mealModalOpen, setMealModalOpen] = useState(false); + const [editedActivity, setEditedActivity] = useState(); + const [activityModalOpen, setActivityModalOpen] = useState(false); + const [workoutModalOpen, setWorkoutModalOpen] = useState(false); + + const loadDay = useCallback(async (forDate: string) => { + setLoading(true); + const response = await getDayOverview({ query: { date: forDate } }); + setDay(response.data ?? emptyOverview(forDate)); + setLoading(false); + }, []); + + useEffect(() => { loadDay(date); }, [date, loadDay]); + + const isToday = date === formatDate(new Date()); + + /** Jídla rozdělená do chodů — zobrazují se jen chody, které něco obsahují. */ + const mealGroups = useMemo(() => MEAL_TYPES_IN_ORDER + .map(mealType => ({ + mealType, + entries: day.meals.entries.filter(entry => entry.mealType === mealType), + })) + .filter(group => group.entries.length > 0), [day.meals.entries]); + + const { energy } = day; + const isDeficit = energy.balance < 0; + + const removeMeal = async (entry: MealEntry) => { + await deleteMeal({ body: { id: entry.id, date: entry.date } }); + loadDay(date); + }; + + const removeActivity = async (entry: ActivityEntry) => { + const response = await deleteActivity({ body: { id: entry.id, date: entry.date } }); + if (response.data) setDay(response.data); + }; + + return ( + <> +
+

Můj den

+ {tab === 'prijem' ? ( + + ) : ( +
+ + +
+ )} +
+ +
+ +
+
{getHumanDate(date)}
+
{isToday ? 'dnes' : getDayOfWeek(date)}
+
+ + event.target.value && setDate(event.target.value)} + /> + {!isToday && ( + + )} +
+ +
+
+
Příjem
+
{formatCalories(energy.intake)}
+
{formatPrice(day.meals.totalPrice)}
+
+
+
Výdej
+
{formatCalories(energy.totalBurn)}
+
+ {energy.hasBasal + ? `klid ${energy.basal} + pohyb ${energy.activityBurn} kcal` + : `pohyb ${energy.activityBurn} kcal`} +
+
+
+
Bilance
+
{formatBalance(energy.balance)}
+
+ {energy.hasBasal + ? (isDeficit ? 'jste v deficitu' : 'jste v přebytku') + : 'nastavte klidový výdej pro skutečný deficit'} +
+
+
+
Útrata
+
{formatPrice(day.meals.totalPrice)}
+
+
+ +
+ + +
+ + {loading && ( +
+ +
+ )} + + {!loading && tab === 'prijem' && ( + <> + {!day.meals.entries.length && ( +
+
+
Za tento den zatím nic nemáte
+

+ Přidejte jídlo ručně, nebo si ve statistikách naimportujte měsíční přehled z Luncheru. +

+
+ )} + {mealGroups.map(group => ( +
+

+ {MEAL_TYPE_ICONS[group.mealType]} + {MEAL_TYPE_NAMES[group.mealType]} + + {formatPrice(group.entries.reduce((sum, entry) => sum + (entry.price ?? 0), 0))} + +

+
+ {group.entries.map(entry => ( + { setEditedMeal(entry); setMealModalOpen(true); }} + onDelete={() => removeMeal(entry)} + /> + ))} +
+
+ ))} + + )} + + {!loading && tab === 'vydej' && ( + <> + {!day.activities.entries.length && ( +
+
+
Za tento den nemáte žádný pohyb
+

+ Přidejte aktivitu, nebo použijte šablonu tréninku a založí se celá naráz. +

+
+ )} + {!!day.activities.entries.length && ( +
+ {day.activities.entries.map(entry => ( + { setEditedActivity(entry); setActivityModalOpen(true); }} + onDelete={() => removeActivity(entry)} + /> + ))} +
+ )} + + )} + + setMealModalOpen(false)} + onSaved={saved => { + setMealModalOpen(false); + // Úprava mohla jídlo přesunout jinam — pak přepneme na jeho nový den + if (saved.date !== date) { + setDate(saved.date); + } else { + loadDay(date); + } + }} + /> + + setActivityModalOpen(false)} + onSaved={saved => { + setActivityModalOpen(false); + if (saved.date !== date) { + setDate(saved.date); + } else { + setDay(saved); + } + }} + /> + + setWorkoutModalOpen(false)} + onApplied={saved => { + setDay(saved); + setWorkoutModalOpen(false); + }} + /> + + ); +} + +type MealRowProps = { + entry: MealEntry, + onEdit: () => void, + onDelete: () => void, +}; + +/** Jeden řádek seznamu jídel. */ +function MealRow({ entry, onEdit, onDelete }: Readonly) { + return ( +
+
+
{entry.name}
+
+ {entry.source && {entry.source}} + {entry.importSource && z Luncheru} + {entry.weight != null && {formatWeight(entry.weight)}} + {entry.calories != null && {formatCalories(entry.calories)}} +
+ {entry.note &&
{entry.note}
} +
+
{formatPrice(entry.price)}
+ +
+ ); +} + +type ActivityRowProps = { + entry: ActivityEntry, + onEdit: () => void, + onDelete: () => void, +}; + +/** Jeden řádek seznamu aktivit. */ +function ActivityRow({ entry, onEdit, onDelete }: Readonly) { + return ( +
+
+
{entry.name}
+
+ {formatQuantity(entry.quantity, ACTIVITY_UNIT_SHORT[entry.unit])} + {entry.templateId && ze šablony} +
+ {entry.note &&
{entry.note}
} +
+
{formatCalories(entry.calories)}
+ +
+ ); +} + +/** Tlačítka pro úpravu a smazání řádku. */ +function RowActions({ onEdit, onDelete, label }: Readonly<{ onEdit: () => void, onDelete: () => void, label: string }>) { + return ( +
+ + +
+ ); +} diff --git a/client/src/pages/StatsPage.scss b/client/src/pages/StatsPage.scss new file mode 100644 index 0000000..d61db5d --- /dev/null +++ b/client/src/pages/StatsPage.scss @@ -0,0 +1,142 @@ +.range-picker { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 16px; + padding: 12px 16px; + + .range-modes { + display: inline-flex; + background: var(--ft-bg-hover); + border-radius: var(--ft-radius-sm); + padding: 3px; + gap: 2px; + } + + .range-mode { + background: transparent; + border: none; + border-radius: calc(var(--ft-radius-sm) - 2px); + padding: 5px 16px; + font-size: 0.87rem; + font-weight: 600; + color: var(--ft-text-secondary); + transition: var(--ft-transition); + + &:hover { + color: var(--ft-text); + } + + &.active { + background: var(--ft-bg-card); + color: var(--ft-primary); + box-shadow: var(--ft-shadow-sm); + } + } + + .range-input { + width: auto; + min-width: 140px; + } +} + +.chart-panel { + margin-bottom: 16px; + + .chart-title { + font-size: 0.85rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--ft-text-secondary); + margin: 0 0 14px; + } + + // Recharts kreslí popisky vlastními , barvu jim musíme vnutit + .recharts-legend-item-text, + .recharts-cartesian-axis-tick-value { + fill: var(--ft-text-secondary) !important; + color: var(--ft-text-secondary) !important; + font-size: 0.8rem; + } +} + +.chart-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 16px; +} + +.source-table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; + + th { + text-align: left; + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--ft-text-muted); + padding-bottom: 8px; + border-bottom: 1px solid var(--ft-border); + } + + td { + padding: 9px 0; + border-bottom: 1px solid var(--ft-border-light); + color: var(--ft-text); + } + + tr:last-child td { + border-bottom: none; + } + + .numeric { + text-align: right; + white-space: nowrap; + } + + td.numeric { + font-variant-numeric: tabular-nums; + font-weight: 600; + } +} + +.import-preview-list { + list-style: none; + margin: 12px 0 0; + padding: 0; + font-size: 0.85rem; + + li { + display: flex; + align-items: baseline; + gap: 8px; + padding: 4px 0; + border-top: 1px solid var(--ft-border-light); + } + + .import-preview-date { + font-variant-numeric: tabular-nums; + color: var(--ft-text-secondary); + flex: 0 0 auto; + } + + .import-preview-price { + margin-left: auto; + font-weight: 600; + white-space: nowrap; + } +} + +.import-warning { + display: flex; + align-items: baseline; + gap: 8px; + font-size: 0.82rem; + color: var(--ft-warning); + padding: 3px 0; +} diff --git a/client/src/pages/StatsPage.tsx b/client/src/pages/StatsPage.tsx new file mode 100644 index 0000000..17e209b --- /dev/null +++ b/client/src/pages/StatsPage.tsx @@ -0,0 +1,255 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Button, Spinner } from "react-bootstrap"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faCoins, faFire, faListCheck, faCalendarDay, faFileImport, faChartColumn } from "@fortawesome/free-solid-svg-icons"; +import { + Bar, BarChart, CartesianGrid, Cell, Pie, PieChart, ResponsiveContainer, Tooltip, XAxis, YAxis, Legend, +} from "recharts"; +import { FoodStats, MealType, getFoodStats } from "../../../types"; +import { MEAL_TYPE_NAMES } from "../enums"; +import { + formatCalories, formatDateString, formatMonth, formatPrice, formatPriceShort, + getHumanMonth, getMonthEnd, getMonthStart, +} from "../Utils"; +import ImportModal from "../components/modals/ImportModal"; +import "./StatsPage.scss"; + +/** Rozsahy, za které lze statistiky zobrazit. */ +type RangeMode = 'month' | 'year' | 'all'; + +const RANGE_LABELS: Record = { + month: 'Měsíc', + year: 'Rok', + all: 'Vše', +}; + +/** Nejstarší rok, do kterého sahá volba "Vše". Starší data nikdo evidovat nebude. */ +const EARLIEST_YEAR = 2015; + +/** Barvy výsečí grafu dle typu jídla — laděné do zelené palety aplikace. */ +const PIE_COLORS = ['#16a34a', '#22c55e', '#4ade80', '#86efac', '#0ea5e9', '#a3a3a3']; + +/** Vzhled bubliny s hodnotou — recharts ji kreslí inline stylem, ne přes CSS třídu. */ +const TOOLTIP_STYLE = { + background: 'var(--ft-bg-card)', + border: '1px solid var(--ft-border)', + borderRadius: 'var(--ft-radius-sm)', + color: 'var(--ft-text)', +}; + +/** Naformátuje hodnotu v bublině grafu na koruny. Hodnoty chodí z rechartsu jako `unknown`. */ +function formatChartPrice(value: unknown): string { + return `${Number(value).toLocaleString('cs-CZ')} Kč`; +} + +export default function StatsPage() { + const [mode, setMode] = useState('month'); + const [month, setMonth] = useState(() => formatMonth(new Date())); + const [year, setYear] = useState(() => new Date().getFullYear()); + const [stats, setStats] = useState(); + const [loading, setLoading] = useState(true); + const [importOpen, setImportOpen] = useState(false); + + /** Přepočte hranice období z aktuálně zvoleného rozsahu. */ + const range = useMemo(() => { + if (mode === 'month') { + return { from: getMonthStart(month), to: getMonthEnd(month) }; + } + if (mode === 'year') { + return { from: `${year}-01-01`, to: `${year}-12-31` }; + } + return { from: `${EARLIEST_YEAR}-01-01`, to: `${new Date().getFullYear()}-12-31` }; + }, [mode, month, year]); + + const load = useCallback(async () => { + setLoading(true); + const response = await getFoodStats({ query: range }); + setStats(response.data); + setLoading(false); + }, [range]); + + useEffect(() => { load(); }, [load]); + + /** Řada pro sloupcový graf — dny u měsíce, měsíce u roku, roky u celého přehledu. */ + const series = useMemo(() => { + if (!stats) return []; + if (mode === 'month') { + return stats.byDay.map(total => ({ + label: formatDateString(total.period).slice(0, 5), + price: total.price / 100, + calories: total.calories, + })); + } + if (mode === 'year') { + return stats.byMonth.map(total => ({ + label: getHumanMonth(total.period).split(' ')[0], + price: total.price / 100, + calories: total.calories, + })); + } + return stats.byYear.map(total => ({ + label: total.period, + price: total.price / 100, + calories: total.calories, + })); + }, [stats, mode]); + + const pieData = useMemo(() => (stats?.byMealType ?? []) + .filter(total => total.price > 0) + .map(total => ({ + name: MEAL_TYPE_NAMES[total.key as MealType] ?? total.key, + value: total.price / 100, + })), [stats]); + + const averagePerDay = stats?.dayCount ? Math.round(stats.totalPrice / stats.dayCount) : 0; + + return ( + <> +
+
+

Statistiky

+

Kolik jste utratili za jídlo a za co

+
+ +
+ +
+
+ {(Object.keys(RANGE_LABELS) as RangeMode[]).map(value => ( + + ))} +
+ {mode === 'month' && ( + event.target.value && setMonth(event.target.value)} + /> + )} + {mode === 'year' && ( + setYear(Number(event.target.value))} + /> + )} +
+ +
+
+
Celkem utraceno
+
{formatPrice(stats?.totalPrice ?? 0)}
+
+
+
Průměr na den
+
{formatPrice(averagePerDay)}
+
z {stats?.dayCount ?? 0} vykázaných dní
+
+
+
Jídel
+
{stats?.entryCount ?? 0}
+
+
+
Kalorie
+
{formatCalories(stats?.totalCalories)}
+
+
+ + {loading && ( +
+ +
+ )} + + {!loading && !stats?.entryCount && ( +
+
+
Za zvolené období nemáte žádná data
+

+ Přidejte jídla v přehledu dne, nebo si sem naimportujte měsíční přehled z Luncheru. +

+
+ )} + + {!loading && !!stats?.entryCount && ( + <> +
+

Útrata v čase

+ + + + + + [formatChartPrice(value), 'Útrata']} + contentStyle={TOOLTIP_STYLE} + /> + + + +
+ +
+
+

Podle chodu

+ + + + {pieData.map((entry, index) => ( + + ))} + + + formatChartPrice(value)} + contentStyle={TOOLTIP_STYLE} + /> + + +
+ +
+

Kde nejvíc utrácíte

+ + + + + + + + + + {stats.bySource.slice(0, 8).map(total => ( + + + + + + ))} + +
ZdrojJídelÚtrata
{total.key}{total.count}{formatPriceShort(total.price)}
+
+
+ + )} + + setImportOpen(false)} + onImported={load} + /> + + ); +} diff --git a/client/src/routes.ts b/client/src/routes.ts new file mode 100644 index 0000000..1b77b98 --- /dev/null +++ b/client/src/routes.ts @@ -0,0 +1,8 @@ +/** + * Adresy stránek aplikace. + * + * Žijí ve vlastním modulu, a ne v AppRoutes — hlavička je potřebuje, AppRoutes + * potřebuje hlavičku, a kruhový import by konstanty nechal při načtení nedefinované. + */ +export const DEN_URL = '/'; +export const STATISTIKY_URL = '/statistiky'; diff --git a/client/tsconfig.json b/client/tsconfig.json new file mode 100644 index 0000000..3eb6b3b --- /dev/null +++ b/client/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "types": ["vite/client"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "moduleResolution": "bundler", + "module": "ESNext", + "target": "ESNext", + "resolveJsonModule": true, + "isolatedModules": true, + "allowImportingTsExtensions": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src", "../types"] +} diff --git a/client/vite.config.ts b/client/vite.config.ts new file mode 100644 index 0000000..73687df --- /dev/null +++ b/client/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import viteTsconfigPaths from 'vite-tsconfig-paths' + +export default defineConfig({ + base: '', + plugins: [react(), viteTsconfigPaths()], + build: { + sourcemap: true, + }, + server: { + open: true, + host: '0.0.0.0', + port: 3000, + proxy: { + '/api': 'http://localhost:3001', + } + }, +}) diff --git a/client/yarn.lock b/client/yarn.lock new file mode 100644 index 0000000..a239dbc --- /dev/null +++ b/client/yarn.lock @@ -0,0 +1,1527 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.7.tgz#f2fbbfea87c44a21590ec515b778b2c26d8866e7" + integrity sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw== + dependencies: + "@babel/helper-validator-identifier" "^7.29.7" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/compat-data@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.7.tgz#6f0237f0f36d2e51c0570a636faed9d2d0efe629" + integrity sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg== + +"@babel/core@^7.29.0": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.7.tgz#80c10b17248082968b57a857b91640971f2070f7" + integrity sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA== + dependencies: + "@babel/code-frame" "^7.29.7" + "@babel/generator" "^7.29.7" + "@babel/helper-compilation-targets" "^7.29.7" + "@babel/helper-module-transforms" "^7.29.7" + "@babel/helpers" "^7.29.7" + "@babel/parser" "^7.29.7" + "@babel/template" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/types" "^7.29.7" + "@jridgewell/remapping" "^2.3.5" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.29.7", "@babel/generator@^7.29.8": + version "7.29.8" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.8.tgz#4b0b887885422643339e09022148a4c4ebaa4979" + integrity sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg== + dependencies: + "@babel/parser" "^7.29.8" + "@babel/types" "^7.29.8" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-compilation-targets@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz#7a1def704302401c47f64fa85589e974ae217042" + integrity sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g== + dependencies: + "@babel/compat-data" "^7.29.7" + "@babel/helper-validator-option" "^7.29.7" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-globals@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz#f04a96fbd8473241b1079243f5b3f03a3010ab7b" + integrity sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA== + +"@babel/helper-module-imports@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz#ef25048a518e828d7393fac5882ddd73921d7396" + integrity sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g== + dependencies: + "@babel/traverse" "^7.29.7" + "@babel/types" "^7.29.7" + +"@babel/helper-module-transforms@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz#b062747a5997ba138637201328bbff77960574ae" + integrity sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg== + dependencies: + "@babel/helper-module-imports" "^7.29.7" + "@babel/helper-validator-identifier" "^7.29.7" + "@babel/traverse" "^7.29.7" + +"@babel/helper-plugin-utils@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz#c0a0766f1a13617d8a17407d7ab8f9d486225ea4" + integrity sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw== + +"@babel/helper-string-parser@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz#7f0871d99824d23137d60f86fcf6130fd5a1b51f" + integrity sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw== + +"@babel/helper-validator-identifier@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz#bd87084ced0c796ec46bda492de6e83d29e89fc2" + integrity sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg== + +"@babel/helper-validator-option@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz#cf315be940213b354eb4abcc0bd01ebe3f73bc2a" + integrity sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw== + +"@babel/helpers@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.29.7.tgz#45abfde7548997e34376c3e69feb475cffb4a607" + integrity sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg== + dependencies: + "@babel/template" "^7.29.7" + "@babel/types" "^7.29.7" + +"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.29.7", "@babel/parser@^7.29.8": + version "7.29.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.8.tgz#9653716a2f10c677b98fbc63d4bfb000c302cf17" + integrity sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA== + dependencies: + "@babel/types" "^7.29.8" + +"@babel/plugin-transform-react-jsx-self@^7.27.1": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz#c24424527858220624fd59a5b1eab4fa413c803a" + integrity sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-react-jsx-source@^7.27.1": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz#5cf25a3689906b58e2f0a2f2b374789e6627b15f" + integrity sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/runtime@^7.24.7", "@babel/runtime@^7.26.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.7.tgz#12022450c45a4da6d8d8287b18a4ff2ddb23f768" + integrity sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw== + +"@babel/template@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.29.7.tgz#4d9d4004f645cdd304de958c725162784ecac700" + integrity sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg== + dependencies: + "@babel/code-frame" "^7.29.7" + "@babel/parser" "^7.29.7" + "@babel/types" "^7.29.7" + +"@babel/traverse@^7.29.7": + version "7.29.8" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.8.tgz#4111014cdc71a0f95d9471907590baa0b8a6b28a" + integrity sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg== + dependencies: + "@babel/code-frame" "^7.29.7" + "@babel/generator" "^7.29.8" + "@babel/helper-globals" "^7.29.7" + "@babel/parser" "^7.29.8" + "@babel/template" "^7.29.7" + "@babel/types" "^7.29.8" + debug "^4.3.1" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.28.2", "@babel/types@^7.29.7", "@babel/types@^7.29.8": + version "7.29.8" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.8.tgz#1229eef31d85156d70fa3f4cd859376d0eaf6863" + integrity sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg== + dependencies: + "@babel/helper-string-parser" "^7.29.7" + "@babel/helper-validator-identifier" "^7.29.7" + +"@esbuild/aix-ppc64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz#bf6e10303bcf2e7c686975fa52f937ec2728d8bc" + integrity sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ== + +"@esbuild/android-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz#0c6246bc8d2c4d172aac2db3fb1190d72bd65504" + integrity sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A== + +"@esbuild/android-arm@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.28.2.tgz#2d84ece6a4e2684d92be26ee13d42757d831c381" + integrity sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg== + +"@esbuild/android-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.28.2.tgz#fc38d4d6358d8dc1cf53f09f7589fe436eb64801" + integrity sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q== + +"@esbuild/darwin-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz#f83afeeac1d7dac01c7a2fd012b3e451a0591fcc" + integrity sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw== + +"@esbuild/darwin-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz#510147c055a795588dbbe14fd6b1b8ad0a2f30de" + integrity sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw== + +"@esbuild/freebsd-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz#093b9200ecf0b115ba4e5e248a7485c9c5f8bd5e" + integrity sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw== + +"@esbuild/freebsd-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz#0be22b6df925d213e841ea87123af5df80b0faf7" + integrity sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg== + +"@esbuild/linux-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz#1bdbc651cda9ba9995c53ed9c71ceaa65094762d" + integrity sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug== + +"@esbuild/linux-arm@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz#beb12ad72b84f72d28488cc1b8ee9f7eb141d753" + integrity sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w== + +"@esbuild/linux-ia32@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz#b81f9d55529b45c206a46a138214b1aa6879696b" + integrity sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ== + +"@esbuild/linux-loong64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz#598667241a04c99b76ed6ef940ac50038c419f98" + integrity sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ== + +"@esbuild/linux-mips64el@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz#1c51eb9cea903f53d97b5af3b1841db70f5596ca" + integrity sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA== + +"@esbuild/linux-ppc64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz#63dd61f17ceb31a81227f413feac8a71bc2c51f2" + integrity sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ== + +"@esbuild/linux-riscv64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz#3763b08fde5cf25ab1facb8e7752edfe45fbfc27" + integrity sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA== + +"@esbuild/linux-s390x@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz#1a137ff293a82906eb3176385bd7e8e0e5cfb7cb" + integrity sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg== + +"@esbuild/linux-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz#268b36211c146ca54f8fe12c578a8d6ef8979485" + integrity sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ== + +"@esbuild/netbsd-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz#22571ad951d62bb6accc82d8d1fad5c8c1ac0ba1" + integrity sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw== + +"@esbuild/netbsd-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz#42fcc57297eb0a0ca3f5fc475291f4c1a3f7c0de" + integrity sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw== + +"@esbuild/openbsd-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz#9eb32af104ac3dacf4edca01f596664aab0c73ef" + integrity sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ== + +"@esbuild/openbsd-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz#febed2402d6088225e91f20fb4ce2522ad0a4efd" + integrity sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw== + +"@esbuild/openharmony-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz#85641c3d466428bfbccea5f21c26836663fef5ce" + integrity sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q== + +"@esbuild/sunos-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz#a736f9d8962481045fc4c3e54f5479f22c870fb4" + integrity sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g== + +"@esbuild/win32-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz#ee5ab40fad186201b652a33f8a5eb149e9e42532" + integrity sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ== + +"@esbuild/win32-ia32@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz#c40d28a6d99a127da6711f2afd74b11cb63b06a7" + integrity sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA== + +"@esbuild/win32-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz#b21affb804cc167c133d95f45b3a1dc1323b9a87" + integrity sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g== + +"@fortawesome/fontawesome-common-types@7.3.1": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.3.1.tgz#913fe0c0aed1184390efeec2546d78f9c4c8dc4a" + integrity sha512-k0C0sdHmZtAo6dRDtd1Z/qcpyHbL0CKsjV8seMY/21xGhY5Wsv0XRmiI/xEEH4y2c9b1+jvgNs/3EqhV27yUEA== + +"@fortawesome/fontawesome-svg-core@^7.1.0": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.3.1.tgz#eca344c1c8094598240c44d28e97ad53eb0c4537" + integrity sha512-BoxVN3PKnMbgStHhjoaky/oWdxHomDqmBVA24IA3KEmssFGeI7u9YT/BJceOjIum/t6TpPa/vcMKaVYQeIQ/3Q== + dependencies: + "@fortawesome/fontawesome-common-types" "7.3.1" + +"@fortawesome/free-regular-svg-icons@^7.1.0": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-7.3.1.tgz#643bf28ff4a878ee75abbf974a6096bbe3816306" + integrity sha512-q1EsmL7Q8DDnkRBUjSvrxbq7c9oVwwVjCn/xa5apKmdp65YSgzUg2y0Ltnd5aDbT6GdAQQdXql5Ha90ArqIReQ== + dependencies: + "@fortawesome/fontawesome-common-types" "7.3.1" + +"@fortawesome/free-solid-svg-icons@^7.1.0": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.3.1.tgz#07d38cd0cffd759ae64c78e40a9a928ecf0cb19a" + integrity sha512-v0BLa0eqg7ubvVWeNSHVBs8fWH/GJicERZoJaxJ3FE/lj67VSqzoMg9pzfZVOfLMX10y0pGwQAuxoRVVH2patg== + dependencies: + "@fortawesome/fontawesome-common-types" "7.3.1" + +"@fortawesome/react-fontawesome@^3.1.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-3.5.0.tgz#ce8152e556809326e1253682088bfdd116a453bd" + integrity sha512-63mlRr6fiBbJ0wjr1Cf6dsDGtP2lNvk9lnatKgxs/fIkhslsZT291hIUzJkuUkI9yr69ZvWnWfgb2qXm4QyVaA== + +"@internationalized/date@^3.12.4": + version "3.12.4" + resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.12.4.tgz#a391e01d54ca80b3cb4c3d5040d46a0b26c3f20f" + integrity sha512-M1dEn4c1U1HsSlaVR8upZtSqvXrTkHDfv18H01uCSJyjVLDxnBR38v/fMxecmlwXKR4i9HeZcmgQAPE6A+aGJQ== + dependencies: + "@swc/helpers" "^0.5.0" + +"@internationalized/number@^3.6.8": + version "3.6.8" + resolved "https://registry.yarnpkg.com/@internationalized/number/-/number-3.6.8.tgz#aaa3e16fb9d64a8d7f130ccf848c626914dfdf9e" + integrity sha512-8UmMFia46DUt+k97zKd9fKWXcWHR+k8ae3eYzILETuT2KbIvLyOfac7zesw+sJdRAAZ7Q9pM1Mk22aXp2LD0Ig== + dependencies: + "@swc/helpers" "^0.5.0" + +"@internationalized/string@^3.2.10": + version "3.2.10" + resolved "https://registry.yarnpkg.com/@internationalized/string/-/string-3.2.10.tgz#c38bd59a69509a41bf7422f1b2fc3af7468e4322" + integrity sha512-PDx6//vHSpRnHfxqMqto11zQvhsaU74O3mKv2F/0eicGZcl9NLjQmGlbHz/LsJh5tLKp4A4L7ZVTzN1/MmMTvA== + dependencies: + "@swc/helpers" "^0.5.0" + +"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz#f4c663e862f06dc98ca4d453862c46902789a18d" + integrity sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.31" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@napi-rs/lzma-linux-x64-gnu@1.5.1": + version "1.5.1" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz#e57d4306966078662038094fb38eb9146dc3aea9" + integrity sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ== + +"@parcel/watcher-android-arm64@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz#99aaa3223d43807c9340af439cad7e9b6d26ada6" + integrity sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA== + +"@parcel/watcher-darwin-arm64@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz#024496e586b4744f09ce532bbe89fe38ef02a64e" + integrity sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw== + +"@parcel/watcher-darwin-x64@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz#a4621df1359a93d39a332d9bab5ff09016a0608f" + integrity sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw== + +"@parcel/watcher-freebsd-x64@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz#7f565ed1a5b3a5e604e6a4799121518265d62a3d" + integrity sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ== + +"@parcel/watcher-linux-arm-glibc@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz#ad7d3825e67b81999165da42593022045abc0889" + integrity sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg== + +"@parcel/watcher-linux-arm-musl@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz#fe7d1cccb2c483215c090e938cf5cf404d2f9a8c" + integrity sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw== + +"@parcel/watcher-linux-arm64-glibc@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz#7e239dcb4646c4c79f006a7131a48238249530da" + integrity sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g== + +"@parcel/watcher-linux-arm64-musl@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz#c58b8d9c6d8d81594be00dd83aab741c1aaf7e0e" + integrity sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA== + +"@parcel/watcher-linux-x64-glibc@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz#5184fa9a770478d86e56875f4ee163a0abdc8791" + integrity sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A== + +"@parcel/watcher-linux-x64-musl@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz#2d1c55aa7246cbc7670e2612058a8a542c9cf246" + integrity sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw== + +"@parcel/watcher-win32-arm64@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz#15e09432040fee9e2213aa9c10ed589012526def" + integrity sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ== + +"@parcel/watcher-win32-x64@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz#9bee199a2a4accd557b451ac2c1c793f305ae012" + integrity sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A== + +"@parcel/watcher@^2.4.1": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.6.0.tgz#99661f6220070b76a766aba6b7e313a087a1be4f" + integrity sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w== + dependencies: + detect-libc "^2.0.3" + is-glob "^4.0.3" + node-addon-api "^7.0.0" + picomatch "^4.0.4" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.6.0" + "@parcel/watcher-darwin-arm64" "2.6.0" + "@parcel/watcher-darwin-x64" "2.6.0" + "@parcel/watcher-freebsd-x64" "2.6.0" + "@parcel/watcher-linux-arm-glibc" "2.6.0" + "@parcel/watcher-linux-arm-musl" "2.6.0" + "@parcel/watcher-linux-arm64-glibc" "2.6.0" + "@parcel/watcher-linux-arm64-musl" "2.6.0" + "@parcel/watcher-linux-x64-glibc" "2.6.0" + "@parcel/watcher-linux-x64-musl" "2.6.0" + "@parcel/watcher-win32-arm64" "2.6.0" + "@parcel/watcher-win32-x64" "2.6.0" + +"@popperjs/core@^2.11.8": + version "2.11.8" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + +"@react-aria/ssr@^3.5.0": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.10.1.tgz#d082600c811e35e8ab780c55c47b5d731359aaf7" + integrity sha512-jn038/ZYmu6DpfXJ6r2U9zFFppjbc9wnApPJSCxao2RZVEqep4YyoniHSy8qv6V21/xyS4IV7W9a+X2jOjSuag== + dependencies: + "@swc/helpers" "^0.5.0" + react-aria "^3.48.0" + +"@react-types/shared@^3.36.1": + version "3.36.1" + resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.36.1.tgz#edb8081e3872ae68a4eb4a65e62233d0754ec186" + integrity sha512-AzsuD9OfxTOZMMvTRhlN3oHBwOmFN7tDh27LzqmHt4+uOgPhJT7ZM7/kVs/8/o0WxayMUIk3hBmCFRHv1FUoag== + +"@reduxjs/toolkit@^1.9.0 || 2.x.x": + version "2.12.0" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.12.0.tgz#e62787503a38561e04bb8f39e29ca8db689590f9" + integrity sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw== + dependencies: + "@standard-schema/spec" "^1.0.0" + "@standard-schema/utils" "^0.3.0" + immer "^11.0.0" + redux "^5.0.1" + redux-thunk "^3.1.0" + reselect "^5.1.0" + +"@restart/hooks@^0.4.9": + version "0.4.16" + resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.4.16.tgz#95ae8ac1cc7e2bd4fed5e39800ff85604c6d59fb" + integrity sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w== + dependencies: + dequal "^2.0.3" + +"@restart/hooks@^0.5.0": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.5.1.tgz#6776b3859e33aea72b23b81fc47021edf17fd247" + integrity sha512-EMoH04NHS1pbn07iLTjIjgttuqb7qu4+/EyhAx27MHpoENcB2ZdSsLTNxmKD+WEPnZigo62Qc8zjGnNxoSE/5Q== + dependencies: + dequal "^2.0.3" + +"@restart/ui@^1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@restart/ui/-/ui-1.9.4.tgz#9d61f56f2647f5ab8a33d87b278b9ce183511a26" + integrity sha512-N4C7haUc3vn4LTwVUPlkJN8Ach/+yIMvRuTVIhjilNHqegY60SGLrzud6errOMNJwSnmYFnt1J0H/k8FE3A4KA== + dependencies: + "@babel/runtime" "^7.26.0" + "@popperjs/core" "^2.11.8" + "@react-aria/ssr" "^3.5.0" + "@restart/hooks" "^0.5.0" + "@types/warning" "^3.0.3" + dequal "^2.0.3" + dom-helpers "^5.2.0" + uncontrollable "^8.0.4" + warning "^4.0.3" + +"@rolldown/pluginutils@1.0.0-rc.3": + version "1.0.0-rc.3" + resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz#8a88cc92a0f741befc7bc109cb1a4c6b9408e1c5" + integrity sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q== + +"@rollup/rollup-android-arm-eabi@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz#d03ba6ea54f9ec80688d153763cd325a2d2a5af6" + integrity sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ== + +"@rollup/rollup-android-arm64@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz#db5e36aa8a955b4b5e0b024d671230edc5cdc191" + integrity sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ== + +"@rollup/rollup-darwin-arm64@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz#1ed1c43922e7b9b5d020ef65d8402e3c81edc86e" + integrity sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q== + +"@rollup/rollup-darwin-x64@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz#0a86e782bf7a546e74f531e395e24fdb45c83527" + integrity sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg== + +"@rollup/rollup-freebsd-arm64@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz#82fa51c540185b5063c8b3c63aac79b15f2801ad" + integrity sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw== + +"@rollup/rollup-freebsd-x64@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz#df1d73b567fd62e0cf21c9b57dff7f44bfd69638" + integrity sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q== + +"@rollup/rollup-linux-arm-gnueabihf@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz#a585ba0418027a5b567693db3982e5e57544a4c7" + integrity sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw== + +"@rollup/rollup-linux-arm-musleabihf@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz#1326f0db22b690a92efd8eaa06e92268dc7d1bb6" + integrity sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw== + +"@rollup/rollup-linux-arm64-gnu@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz#9491939f7cc43a5b26a877417faeafe69bac79ac" + integrity sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg== + +"@rollup/rollup-linux-arm64-musl@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz#a53d93dac32acc671324af1153930ab5a04d8639" + integrity sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw== + +"@rollup/rollup-linux-loong64-gnu@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz#db6e06173efc870be49a2df692d0c0607417a679" + integrity sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ== + +"@rollup/rollup-linux-loong64-musl@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz#a60734a3de407bcf4bfe44d0b64222c0b9fb30bc" + integrity sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA== + +"@rollup/rollup-linux-ppc64-gnu@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz#28a67e15d7ba8630ef044980a39626e0627d6e73" + integrity sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA== + +"@rollup/rollup-linux-ppc64-musl@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz#77bb553a514942af54070756763dbb44c9c6cb2b" + integrity sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA== + +"@rollup/rollup-linux-riscv64-gnu@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz#ef9f31b917e3b310eac5b86d3b6626df7e543e3d" + integrity sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w== + +"@rollup/rollup-linux-riscv64-musl@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz#68cf61a2fc02171d1fa62568b73c1d942f82e80c" + integrity sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ== + +"@rollup/rollup-linux-s390x-gnu@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz#92d393ca47da0d03d1c1cffb3d7340f26c3a53d2" + integrity sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A== + +"@rollup/rollup-linux-x64-gnu@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz#f6e5c5c51f96ae298617fa26da54675acd60e3bc" + integrity sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w== + +"@rollup/rollup-linux-x64-musl@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz#227a949c481909c781d8a39c280f75553cdd16bc" + integrity sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow== + +"@rollup/rollup-openbsd-x64@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz#f57241ebdb73d3bc236e7b1252988408b2139c13" + integrity sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA== + +"@rollup/rollup-openharmony-arm64@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz#3a9ffe5af71e8316dd2716b57dd64287a8c1fa0b" + integrity sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw== + +"@rollup/rollup-win32-arm64-msvc@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz#7d4a40396ae79ebc1e3636c1d566c7d1838b800c" + integrity sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg== + +"@rollup/rollup-win32-ia32-msvc@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz#f234ab80141da45ebe85727f714eb20de2e72c2a" + integrity sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg== + +"@rollup/rollup-win32-x64-gnu@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz#5d5a664c23c8ff0526b9abd703b50ffe09703c2a" + integrity sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg== + +"@rollup/rollup-win32-x64-msvc@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz#cd19d691330cbd52ebb13620acb6cf7140b95e80" + integrity sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w== + +"@standard-schema/spec@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8" + integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w== + +"@standard-schema/utils@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b" + integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g== + +"@swc/helpers@^0.5.0": + version "0.5.23" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.23.tgz#19287d0d86d962b111376039a50c792902c9a86a" + integrity sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw== + dependencies: + tslib "^2.8.0" + +"@types/babel__core@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.27.0.tgz#b5819294c51179957afaec341442f9341e4108a9" + integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz#07d713d6cce0d265c9849db0cbe62d3f61f36f74" + integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q== + dependencies: + "@babel/types" "^7.28.2" + +"@types/d3-array@^3.0.3": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.2.tgz#e02151464d02d4a1b44646d0fcdb93faf88fde8c" + integrity sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw== + +"@types/d3-color@*": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" + integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== + +"@types/d3-ease@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" + integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== + +"@types/d3-interpolate@^3.0.1": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" + integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.1.tgz#f632b380c3aca1dba8e34aa049bcd6a4af23df8a" + integrity sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg== + +"@types/d3-scale@^4.0.2": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.9.tgz#57a2f707242e6fe1de81ad7bfcccaaf606179afb" + integrity sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw== + dependencies: + "@types/d3-time" "*" + +"@types/d3-shape@^3.1.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.2.0.tgz#66ff342011dc243c6c20e6b899523d148aca412d" + integrity sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time@*", "@types/d3-time@^3.0.0": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f" + integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g== + +"@types/d3-timer@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" + integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== + +"@types/estree@1.0.9": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.9.tgz#cf3f0e876d7bee15a93ab925b82bf570a3904a24" + integrity sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg== + +"@types/node@^24.10.0": + version "24.13.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.13.3.tgz#49f18bd3c647866dcda51a0756c145e14590ce16" + integrity sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q== + dependencies: + undici-types "~7.18.0" + +"@types/prop-types@^15.7.12": + version "15.7.15" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7" + integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw== + +"@types/react-dom@^19.2.2": + version "19.2.7" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.2.7.tgz#eba78fc6201e564986ec40d56c86c8434593cace" + integrity sha512-I8bPpDLcHBv1qiIiXDCy71Rt8eQDKJP0sMSWJphDdAcdqiJ1sGpZamavoEIRZmYzjia9LuEb2HlYdDpmoENpvQ== + +"@types/react-transition-group@^4.4.6": + version "4.4.12" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044" + integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== + +"@types/react@>=16.9.11", "@types/react@^19.2.2": + version "19.2.18" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.18.tgz#eb0b6a1fb635d1a9692d5f84a3495bd8ad153707" + integrity sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w== + dependencies: + csstype "^3.2.2" + +"@types/use-sync-external-store@^0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc" + integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg== + +"@types/warning@^3.0.3": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.4.tgz#ebc0c83180dc83994d902bbd51ab0af8a445b1f9" + integrity sha512-CqN8MnISMwQbLJXO3doBAV4Yw9hx9/Pyr2rZ78+NfaCnhyRA/nKrpyk6E7mKw17ZOaQdLpK9GiUjrqLzBlN3sg== + +"@vitejs/plugin-react@^5.1.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz#108bd0f566f288ce3566982df4eff137ded7b15f" + integrity sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw== + dependencies: + "@babel/core" "^7.29.0" + "@babel/plugin-transform-react-jsx-self" "^7.27.1" + "@babel/plugin-transform-react-jsx-source" "^7.27.1" + "@rolldown/pluginutils" "1.0.0-rc.3" + "@types/babel__core" "^7.20.5" + react-refresh "^0.18.0" + +aria-hidden@^1.2.3: + version "1.2.6" + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.6.tgz#73051c9b088114c795b1ea414e9c0fff874ffc1a" + integrity sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA== + dependencies: + tslib "^2.0.0" + +baseline-browser-mapping@^2.11.20: + version "2.11.21" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz#99af73cb8e54007e4f5345e132278e26c2662f2c" + integrity sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ== + +bootstrap@^5.3.8: + version "5.3.8" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.8.tgz#6401a10057a22752d21f4e19055508980656aeed" + integrity sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg== + +browserslist@^4.24.0: + version "4.28.9" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.9.tgz#07ce6b449b90af880eb9bfb7cd39372cc4f71c8c" + integrity sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg== + dependencies: + baseline-browser-mapping "^2.11.20" + caniuse-lite "^1.0.30001810" + electron-to-chromium "^1.5.420" + node-releases "^2.0.54" + update-browserslist-db "^1.3.2" + +caniuse-lite@^1.0.30001810: + version "1.0.30001810" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz#4970b477dea3278374de9bc43aa8f5d39fc3cda2" + integrity sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg== + +chokidar@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-5.0.0.tgz#949c126a9238a80792be9a0265934f098af369a5" + integrity sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw== + dependencies: + readdirp "^5.0.0" + +classnames@^2.3.2: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + +clsx@^2.0.0, clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + +convert-source-map@^2.0.0: + version "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@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.1.1.tgz#3bb9bdfc82369db9c2f69c93c9c3ceb310c88b3c" + integrity sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ== + +csstype@^3.0.2, csstype@^3.2.2: + version "3.2.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" + integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== + +"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6: + version "3.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" + integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== + dependencies: + internmap "1 - 2" + +"d3-color@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== + +d3-ease@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + +"d3-format@1 - 3": + version "3.1.2" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.2.tgz#01fdb46b58beb1f55b10b42ad70b6e344d5eb2ae" + integrity sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg== + +"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + +d3-path@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + +d3-scale@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" + integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== + dependencies: + d3-array "2.10.0 - 3" + d3-format "1 - 3" + d3-interpolate "1.2.0 - 3" + d3-time "2.1.1 - 3" + d3-time-format "2 - 4" + +d3-shape@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" + integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== + dependencies: + d3-path "^3.1.0" + +"d3-time-format@2 - 4": + version "4.1.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" + integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== + dependencies: + d3-array "2 - 3" + +d3-timer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +decimal.js-light@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" + integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== + +dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + +detect-libc@^2.0.3: + version "2.1.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + +dom-helpers@^5.0.1, dom-helpers@^5.2.0, dom-helpers@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + +electron-to-chromium@^1.5.420: + version "1.5.422" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.422.tgz#e27fb1ca0e6bef612a647022e1469701fea9ee1f" + integrity sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA== + +es-toolkit@^1.39.3: + version "1.52.0" + resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.52.0.tgz#71eaf1a8b18834ef77637eccbb885ba4c03cd6dd" + integrity sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA== + +"esbuild@^0.27.0 || ^0.28.0": + version "0.28.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.28.2.tgz#0f43bd1bad955b72d24e2261e3abe5957ccf0816" + integrity sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA== + optionalDependencies: + "@esbuild/aix-ppc64" "0.28.2" + "@esbuild/android-arm" "0.28.2" + "@esbuild/android-arm64" "0.28.2" + "@esbuild/android-x64" "0.28.2" + "@esbuild/darwin-arm64" "0.28.2" + "@esbuild/darwin-x64" "0.28.2" + "@esbuild/freebsd-arm64" "0.28.2" + "@esbuild/freebsd-x64" "0.28.2" + "@esbuild/linux-arm" "0.28.2" + "@esbuild/linux-arm64" "0.28.2" + "@esbuild/linux-ia32" "0.28.2" + "@esbuild/linux-loong64" "0.28.2" + "@esbuild/linux-mips64el" "0.28.2" + "@esbuild/linux-ppc64" "0.28.2" + "@esbuild/linux-riscv64" "0.28.2" + "@esbuild/linux-s390x" "0.28.2" + "@esbuild/linux-x64" "0.28.2" + "@esbuild/netbsd-arm64" "0.28.2" + "@esbuild/netbsd-x64" "0.28.2" + "@esbuild/openbsd-arm64" "0.28.2" + "@esbuild/openbsd-x64" "0.28.2" + "@esbuild/openharmony-arm64" "0.28.2" + "@esbuild/sunos-x64" "0.28.2" + "@esbuild/win32-arm64" "0.28.2" + "@esbuild/win32-ia32" "0.28.2" + "@esbuild/win32-x64" "0.28.2" + +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +eventemitter3@^5.0.1: + version "5.0.4" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.4.tgz#a86d66170433712dde814707ac52b5271ceb1feb" + integrity sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw== + +fdir@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + +fsevents@^2.3.2, fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +globrex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" + integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== + +immer@^11.0.0, immer@^11.1.8: + version "11.1.18" + resolved "https://registry.yarnpkg.com/immer/-/immer-11.1.18.tgz#87d9bced1e25157dc23bced66811de89460ec8f3" + integrity sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ== + +immutable@^5.1.5: + version "5.1.9" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.9.tgz#ac23c3a01992ab665e14ac9ffff298f28cd74a0c" + integrity sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg== + +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +loose-envify@^1.0.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@^3.3.18: + version "3.3.18" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.18.tgz#f66a2de1199ffde0fcf21c8a5f13106b1c081913" + integrity sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w== + +node-addon-api@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" + integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== + +node-releases@^2.0.54: + version "2.0.54" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.54.tgz#09af17d5647aa9f221ec5cf2becb95b68a981afe" + integrity sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ== + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^4.0.3, picomatch@^4.0.4: + version "4.0.7" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.7.tgz#6313360034ccb36b3dc61ecbdff78121f90fe21f" + integrity sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA== + +postcss@^8.5.6: + version "8.5.28" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.28.tgz#da4563a99a06e62d6c1cd1acae363224bcaed6e9" + integrity sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A== + dependencies: + nanoid "^3.3.18" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +prettier@^3.6.2: + version "3.9.6" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.9.6.tgz#b3ea5146515d40fc53f18aa63f74dfab1e10dbf6" + integrity sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g== + +prop-types-extra@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/prop-types-extra/-/prop-types-extra-1.1.1.tgz#58c3b74cbfbb95d304625975aa2f0848329a010b" + integrity sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew== + dependencies: + react-is "^16.3.2" + warning "^4.0.0" + +prop-types@^15.6.2, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +react-aria@^3.48.0: + version "3.52.1" + resolved "https://registry.yarnpkg.com/react-aria/-/react-aria-3.52.1.tgz#2dfc540b1f97c56c7ba5c47a7ff56ba5646ef589" + integrity sha512-fdZZruC9/x/joCg0mhKGs5aHpwrXLCSZ4GOJmhhYyiE0ffyEsk9MLFt9LCOCF9tn8ErTD+UDQP4oDUSlZkmpCg== + dependencies: + "@internationalized/date" "^3.12.4" + "@internationalized/number" "^3.6.8" + "@internationalized/string" "^3.2.10" + "@react-types/shared" "^3.36.1" + "@swc/helpers" "^0.5.0" + aria-hidden "^1.2.3" + clsx "^2.0.0" + react-stately "3.50.0" + use-sync-external-store "^1.6.0" + +react-bootstrap@^2.10.10: + version "2.10.10" + resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-2.10.10.tgz#be0b0d951a69987152d75c0e6986c80425efdf21" + integrity sha512-gMckKUqn8aK/vCnfwoBpBVFUGT9SVQxwsYrp9yDHt0arXMamxALerliKBxr1TPbntirK/HGrUAHYbAeQTa9GHQ== + dependencies: + "@babel/runtime" "^7.24.7" + "@restart/hooks" "^0.4.9" + "@restart/ui" "^1.9.4" + "@types/prop-types" "^15.7.12" + "@types/react-transition-group" "^4.4.6" + classnames "^2.3.2" + dom-helpers "^5.2.1" + invariant "^2.2.4" + prop-types "^15.8.1" + prop-types-extra "^1.1.0" + react-transition-group "^4.4.5" + uncontrollable "^7.2.1" + warning "^4.0.3" + +react-dom@^19.2.0: + version "19.2.8" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.8.tgz#3b46b9eeda877cdff2cf13d2770fff4ae36c2ec2" + integrity sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ== + dependencies: + scheduler "^0.27.0" + +react-is@^16.13.1, react-is@^16.3.2: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-jwt@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/react-jwt/-/react-jwt-1.3.0.tgz#f5f68ae6737a6c1c7197aac813971c40479c26c3" + integrity sha512-aC+X6q8pi63zoO7A060/4mfF5jM6Ay+4YyY4QgdD8dDOqp89sPcg0IhWEHyPACnVETMjBWzmxMPgIPosQNeYyw== + optionalDependencies: + fsevents "^2.3.2" + +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +"react-redux@8.x.x || 9.x.x": + version "9.3.0" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.3.0.tgz#a30113bb6d95c0a715d54dda4308d450fca6ce09" + integrity sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g== + dependencies: + "@types/use-sync-external-store" "^0.0.6" + use-sync-external-store "^1.4.0" + +react-refresh@^0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.18.0.tgz#2dce97f4fe932a4d8142fa1630e475c1729c8062" + integrity sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw== + +react-router-dom@^7.9.5: + version "7.18.3" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.18.3.tgz#0688c91b4b376c8b0dcaf0a0064975d52a32e0f7" + integrity sha512-ytVbyBBM7vMfRCam25r0WMhSVSom909A8p+8m0/f1w853dz/xfFu6etAT2SEbVoSnI+ZoPRDqIsQXVT89gp7kg== + dependencies: + react-router "7.18.3" + +react-router@7.18.3, react-router@^7.9.5: + version "7.18.3" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.18.3.tgz#2a3257aa7c5edd5a71f878063e4c7f3fcfc4b76a" + integrity sha512-gyXgtdr5uACJ5b1Q4udzjVV+tb/rlHIMJKuJ0e89R4Kzgz47z/rgP0dIKxktqIEUhDHluGTPJJH/wRha7CyqsA== + dependencies: + cookie "^1.0.1" + set-cookie-parser "^2.6.0" + +react-stately@3.50.0: + version "3.50.0" + resolved "https://registry.yarnpkg.com/react-stately/-/react-stately-3.50.0.tgz#e94058a2ada3f1d5e2f0591818d18df26129af67" + integrity sha512-TnckvpDQGU0672wEaLZqdzvhuSeXWjgW6vijMWxiKOxc8CosQUfPGISdcZyfHqjX3XMjEtRxj4w9HumvX4h4mw== + dependencies: + "@internationalized/date" "^3.12.4" + "@internationalized/number" "^3.6.8" + "@internationalized/string" "^3.2.10" + "@react-types/shared" "^3.36.1" + "@swc/helpers" "^0.5.0" + use-sync-external-store "^1.6.0" + +react-toastify@^11.0.5: + version "11.1.0" + resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-11.1.0.tgz#3d3c73a44d5cac868ee9a52e0f90dd706532c4a4" + integrity sha512-e9h23x3phN0wbFeB6yovmWp7lobzV4CaCH0LO8nVP6H7Y+3GbcLpIzMm9dJhcp1RXbpyfvjgpfXqO80QAmn7sg== + dependencies: + clsx "^2.1.1" + +react-transition-group@^4.4.5: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + +react@^19.2.0: + version "19.2.8" + resolved "https://registry.yarnpkg.com/react/-/react-19.2.8.tgz#a80663dbb58d69c6fe3fd291d3cb324e8a7dff2d" + integrity sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw== + +readdirp@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-5.1.1.tgz#520bca06f9d1ae1b96cc0800dbe84b983d19422c" + integrity sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA== + +recharts@^3.4.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-3.10.1.tgz#e6f52c2604a38f728b6d47f95881595fa6604c6c" + integrity sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA== + dependencies: + "@reduxjs/toolkit" "^1.9.0 || 2.x.x" + clsx "^2.1.1" + decimal.js-light "^2.5.1" + es-toolkit "^1.39.3" + eventemitter3 "^5.0.1" + immer "^11.1.8" + react-redux "8.x.x || 9.x.x" + reselect "5.2.0" + tiny-invariant "^1.3.3" + use-sync-external-store "^1.2.2" + victory-vendor "^37.0.2" + +redux-thunk@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3" + integrity sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw== + +redux@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b" + integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== + +reselect@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.2.0.tgz#f380ef7664332d26ea06c1cba04bdbbdcaa955f1" + integrity sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw== + +reselect@^5.1.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.3.0.tgz#0a3e3ed4436bdf2ab7c5e0f392dab2c062595d61" + integrity sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg== + +rollup@^4.43.0: + version "4.63.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.63.1.tgz#a9b96d5b2558d034babb12ad8b67a043bc870ac4" + integrity sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg== + dependencies: + "@types/estree" "1.0.9" + optionalDependencies: + "@napi-rs/lzma-linux-x64-gnu" "1.5.1" + "@rollup/rollup-android-arm-eabi" "4.63.1" + "@rollup/rollup-android-arm64" "4.63.1" + "@rollup/rollup-darwin-arm64" "4.63.1" + "@rollup/rollup-darwin-x64" "4.63.1" + "@rollup/rollup-freebsd-arm64" "4.63.1" + "@rollup/rollup-freebsd-x64" "4.63.1" + "@rollup/rollup-linux-arm-gnueabihf" "4.63.1" + "@rollup/rollup-linux-arm-musleabihf" "4.63.1" + "@rollup/rollup-linux-arm64-gnu" "4.63.1" + "@rollup/rollup-linux-arm64-musl" "4.63.1" + "@rollup/rollup-linux-loong64-gnu" "4.63.1" + "@rollup/rollup-linux-loong64-musl" "4.63.1" + "@rollup/rollup-linux-ppc64-gnu" "4.63.1" + "@rollup/rollup-linux-ppc64-musl" "4.63.1" + "@rollup/rollup-linux-riscv64-gnu" "4.63.1" + "@rollup/rollup-linux-riscv64-musl" "4.63.1" + "@rollup/rollup-linux-s390x-gnu" "4.63.1" + "@rollup/rollup-linux-x64-gnu" "4.63.1" + "@rollup/rollup-linux-x64-musl" "4.63.1" + "@rollup/rollup-openbsd-x64" "4.63.1" + "@rollup/rollup-openharmony-arm64" "4.63.1" + "@rollup/rollup-win32-arm64-msvc" "4.63.1" + "@rollup/rollup-win32-ia32-msvc" "4.63.1" + "@rollup/rollup-win32-x64-gnu" "4.63.1" + "@rollup/rollup-win32-x64-msvc" "4.63.1" + fsevents "~2.3.2" + +sass@^1.93.3: + version "1.104.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.104.0.tgz#40a6dfc42f8cc469f7f2c10b40c6a0b8577895ac" + integrity sha512-btHMApW2bgolvClhRW8AlQJzgI9lUB3pPSofBQQT+E46GWvf9o0TVQ13SYv5riWZVFyPN+JNz3TKW9XhBlc10w== + dependencies: + chokidar "^5.0.0" + immutable "^5.1.5" + source-map-js ">=0.6.2 <2.0.0" + optionalDependencies: + "@parcel/watcher" "^2.4.1" + +scheduler@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd" + integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== + +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +set-cookie-parser@^2.6.0: + version "2.7.2" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz#ccd08673a9ae5d2e44ea2a2de25089e67c7edf68" + integrity sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw== + +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +tiny-invariant@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== + +tinyglobby@^0.2.15: + version "0.2.17" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631" + integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.4" + +tsconfck@^3.0.3: + version "3.1.6" + resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.6.tgz#da1f0b10d82237ac23422374b3fce1edb23c3ead" + integrity sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w== + +tslib@^2.0.0, tslib@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +typescript@^5.9.3: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + +uncontrollable@^7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-7.2.1.tgz#1fa70ba0c57a14d5f78905d533cf63916dc75738" + integrity sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ== + dependencies: + "@babel/runtime" "^7.6.3" + "@types/react" ">=16.9.11" + invariant "^2.2.4" + react-lifecycles-compat "^3.0.4" + +uncontrollable@^8.0.4: + version "8.0.4" + resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-8.0.4.tgz#a0a8307f638795162fafd0550f4a1efa0f8c5eb6" + integrity sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ== + +undici-types@~7.18.0: + version "7.18.2" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.18.2.tgz#29357a89e7b7ca4aef3bf0fd3fd0cd73884229e9" + integrity sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w== + +update-browserslist-db@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz#9d99fbff56c50bb11ba5fd35cece5916da595836" + integrity sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0, use-sync-external-store@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d" + integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== + +victory-vendor@^37.0.2: + version "37.3.6" + resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-37.3.6.tgz#401ac4b029a0b3d33e0cba8e8a1d765c487254da" + integrity sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ== + dependencies: + "@types/d3-array" "^3.0.3" + "@types/d3-ease" "^3.0.0" + "@types/d3-interpolate" "^3.0.1" + "@types/d3-scale" "^4.0.2" + "@types/d3-shape" "^3.1.0" + "@types/d3-time" "^3.0.0" + "@types/d3-timer" "^3.0.0" + d3-array "^3.1.6" + d3-ease "^3.0.1" + d3-interpolate "^3.0.1" + d3-scale "^4.0.2" + d3-shape "^3.1.0" + d3-time "^3.0.0" + d3-timer "^3.0.1" + +vite-tsconfig-paths@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz#d9a71106a7ff2c1c840c6f1708042f76a9212ed4" + integrity sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w== + dependencies: + debug "^4.1.1" + globrex "^0.1.2" + tsconfck "^3.0.3" + +vite@^7.2.2: + version "7.3.6" + resolved "https://registry.yarnpkg.com/vite/-/vite-7.3.6.tgz#0547a395e68d3746e9a505f1fd4469fe09b49cc4" + integrity sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg== + dependencies: + esbuild "^0.27.0 || ^0.28.0" + fdir "^6.5.0" + picomatch "^4.0.3" + postcss "^8.5.6" + rollup "^4.43.0" + tinyglobby "^0.2.15" + optionalDependencies: + fsevents "~2.3.3" + +warning@^4.0.0, warning@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..49a4acd --- /dev/null +++ b/compose.yml @@ -0,0 +1,31 @@ +services: + app: + build: . + ports: + - "3001:3001" + environment: + NODE_ENV: production + JWT_SECRET: ${JWT_SECRET:?JWT_SECRET musí být nastaven, minimálně 32 znaků} + STORAGE: redis + REDIS_HOST: redis + REDIS_PORT: 6379 + TZ: Europe/Prague + depends_on: + redis: + condition: service_healthy + restart: unless-stopped + + redis: + image: redis:7-alpine + command: redis-server --appendonly yes + volumes: + - redis-data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 5 + restart: unless-stopped + +volumes: + redis-data: diff --git a/run_dev.ps1 b/run_dev.ps1 new file mode 100644 index 0000000..35a2e57 --- /dev/null +++ b/run_dev.ps1 @@ -0,0 +1,14 @@ +# Spustí vývojové prostředí — server na 3001, klienta na 3000, každý ve svém okně. +$root = $PSScriptRoot + +Start-Process powershell -ArgumentList @( + '-NoExit', '-Command', + "Set-Location '$root\server'; `$env:NODE_ENV='development'; yarn startReload" +) + +Start-Process powershell -ArgumentList @( + '-NoExit', '-Command', + "Set-Location '$root\client'; yarn start" +) + +Write-Host 'Server: http://localhost:3001, klient: http://localhost:3000' diff --git a/run_dev.sh b/run_dev.sh new file mode 100644 index 0000000..332818b --- /dev/null +++ b/run_dev.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Spustí vývojové prostředí v tmuxu — server na 3001, klienta na 3000. +set -e +cd "$(dirname "$0")" + +if ! command -v tmux &> /dev/null; then + echo "tmux není nainstalovaný. Spusťte server a klienta ručně ve dvou terminálech:" + echo " cd server && NODE_ENV=development yarn startReload" + echo " cd client && yarn start" + exit 1 +fi + +SESSION=food-tracer +tmux kill-session -t $SESSION 2>/dev/null || true +tmux new-session -d -s $SESSION -n dev +tmux send-keys -t $SESSION "cd server && NODE_ENV=development yarn startReload" C-m +tmux split-window -h -t $SESSION +tmux send-keys -t $SESSION "cd client && yarn start" C-m +tmux attach -t $SESSION diff --git a/server/.env.development b/server/.env.development new file mode 100644 index 0000000..0458e01 --- /dev/null +++ b/server/.env.development @@ -0,0 +1,4 @@ +JWT_SECRET=vyvojovy-klic-ktery-ma-aspon-32-znaku-delky +STORAGE=json +PORT=3001 +HOST=0.0.0.0 diff --git a/server/.env.template b/server/.env.template new file mode 100644 index 0000000..1d7440a --- /dev/null +++ b/server/.env.template @@ -0,0 +1,16 @@ +# Tajný klíč pro podepisování JWT tokenů, minimálně 32 znaků +JWT_SECRET= +# Úložiště dat: json (výchozí, soubor server/data/db.json), redis nebo memory (testy) +STORAGE=json +# Připojení k Redisu, pokud STORAGE=redis +REDIS_HOST=localhost +REDIS_PORT=6379 +# Přihlášení z hlavičky reverzní proxy +HTTP_REMOTE_USER_ENABLED=false +HTTP_REMOTE_USER_HEADER_NAME=remote-user +HTTP_REMOTE_TRUSTED_IPS= +LOGOUT_URL= +# Port a adresa serveru +PORT=3001 +HOST=0.0.0.0 +CALORIE_PROVIDER= # 'none' vypne externí dotazy na energetické hodnoty diff --git a/server/babel.config.js b/server/babel.config.js new file mode 100644 index 0000000..faa86eb --- /dev/null +++ b/server/babel.config.js @@ -0,0 +1,6 @@ +module.exports = { + presets: [ + ['@babel/preset-env', { targets: { node: 'current' } }], + '@babel/preset-typescript', + ], +}; diff --git a/server/jest.config.js b/server/jest.config.js new file mode 100644 index 0000000..d16c9e0 --- /dev/null +++ b/server/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + testEnvironment: 'node', + setupFiles: ['/src/tests/setupEnv.ts'], + testMatch: ['/src/tests/**/*.test.ts'], +}; diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..da6dfeb --- /dev/null +++ b/server/package.json @@ -0,0 +1,39 @@ +{ + "name": "@food-tracer/server", + "version": "1.0.0", + "main": "src/index.ts", + "license": "MIT", + "private": true, + "scripts": { + "start": "ts-node src/index.ts", + "startReload": "nodemon --watch src src/index.ts", + "build": "tsc -p .", + "test": "jest" + }, + "devDependencies": { + "@babel/core": "^7.28.5", + "@babel/preset-env": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.5", + "@types/jest": "^30.0.0", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^24.10.0", + "@types/supertest": "^6.0.0", + "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" + }, + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "exceljs": "^4.4.0", + "express": "^5.1.0", + "jsonwebtoken": "^9.0.0", + "redis": "^5.9.0", + "simple-json-db": "^2.0.0" + } +} diff --git a/server/src/activities.ts b/server/src/activities.ts new file mode 100644 index 0000000..3e7d222 --- /dev/null +++ b/server/src/activities.ts @@ -0,0 +1,248 @@ +import { randomUUID } from 'crypto'; +import { ActivityDay, ActivityEntry, ActivityUnit } from "../../types/gen/types.gen"; +import getStorage from "./storage"; +import { BadRequestError, NotFoundError, isValidIsoDate } from "./utils"; + +const storage = getStorage(); + +/** Maximální délka textových polí. */ +const MAX_TEXT_LENGTH = 500; + +/** Podporované jednotky aktivit. */ +const UNITS: ActivityUnit[] = [ + ActivityUnit.KROKY, + ActivityUnit.MINUTY, + ActivityUnit.KM, + ActivityUnit.OPAKOVANI, +]; + +/** Vrátí true, pokud je hodnota podporovanou jednotkou aktivity. */ +export function isActivityUnit(value: unknown): value is ActivityUnit { + return typeof value === 'string' && (UNITS as string[]).includes(value); +} + +/** Klíč úložiště s aktivitami jednoho dne jednoho uživatele. */ +function getDayKey(login: string, date: string): string { + return `activities:${login}:${date}`; +} + +/** Sestaví přehled pohybu dne včetně součtu spálených kcal. */ +export function buildActivityDay(date: string, entries: ActivityEntry[]): ActivityDay { + const sorted = [...entries].sort((a, b) => a.createdAt.localeCompare(b.createdAt)); + return { + date, + entries: sorted, + totalCalories: sorted.reduce((sum, entry) => sum + (entry.calories ?? 0), 0), + }; +} + +/** + * Vrátí pohyb uživatele za jeden den. + * + * @param login přihlašovací jméno uživatele + * @param date datum ve formátu YYYY-MM-DD + */ +export async function getActivityDay(login: string, date: string): Promise { + const entries = await storage.getData(getDayKey(login, date)); + return buildActivityDay(date, entries ?? []); +} + +/** Ořízne a znormalizuje volitelný textový vstup. */ +function normalizeText(value: unknown, name: string): string | undefined { + if (value == null) return undefined; + if (typeof value !== 'string') { + throw new BadRequestError(`Pole '${name}' musí být text`); + } + const trimmed = value.trim(); + if (!trimmed.length) return undefined; + if (trimmed.length > MAX_TEXT_LENGTH) { + throw new BadRequestError(`Pole '${name}' může mít nejvýše ${MAX_TEXT_LENGTH} znaků`); + } + return trimmed; +} + +/** Ověří volitelnou nezápornou celočíselnou hodnotu. */ +function normalizeNumber(value: unknown, name: string): number | undefined { + if (value == null) return undefined; + if (typeof value !== 'number' || !Number.isFinite(value)) { + throw new BadRequestError(`Pole '${name}' musí být číslo`); + } + const rounded = Math.round(value); + if (rounded < 0) { + throw new BadRequestError(`Pole '${name}' nesmí být záporné`); + } + return rounded; +} + +/** Znormalizovaný vstup aktivity. */ +export type NormalizedActivityInput = { + date: string; + name: string; + unit: ActivityUnit; + quantity: number; + caloriesPer100Units?: number; + calories?: number; + note?: string; +}; + +/** + * Dopočte spálené kalorie z množství a sazby na 100 jednotek. + * + * Sto jednotek místo jedné proto, že u kroků by sazba na jeden krok byla + * zlomek (~0,04 kcal) a celá aplikace pracuje s celými čísly. + */ +export function deriveActivityCalories(input: NormalizedActivityInput): NormalizedActivityInput { + const calories = input.caloriesPer100Units + ? Math.round(input.quantity / 100 * input.caloriesPer100Units) + : input.calories; + return { ...input, calories }; +} + +/** Ověří a znormalizuje vstupní data aktivity. */ +export function normalizeActivityInput(input: unknown): NormalizedActivityInput { + const value = (input ?? {}) as Record; + if (!isValidIsoDate(value.date)) { + throw new BadRequestError("Nebylo předáno platné datum ve formátu YYYY-MM-DD"); + } + if (!isActivityUnit(value.unit)) { + throw new BadRequestError("Nebyla předána platná jednotka aktivity"); + } + const name = normalizeText(value.name, 'name'); + if (!name) { + throw new BadRequestError("Nebyl předán název aktivity"); + } + const quantity = normalizeNumber(value.quantity, 'quantity'); + if (quantity == null) { + throw new BadRequestError("Nebylo předáno množství aktivity"); + } + return deriveActivityCalories({ + date: value.date, + name, + unit: value.unit, + quantity, + caloriesPer100Units: normalizeNumber(value.caloriesPer100Units, 'caloriesPer100Units'), + calories: normalizeNumber(value.calories, 'calories'), + note: normalizeText(value.note, 'note'), + }); +} + +/** + * Přidá aktivitu do zvoleného dne. + * + * @param login přihlašovací jméno uživatele + * @param input data aktivity + * @returns datum, do kterého se aktivita založila + */ +export async function addActivity(login: string, input: unknown): Promise { + const normalized = normalizeActivityInput(input); + const entry: ActivityEntry = { + id: randomUUID(), + createdAt: new Date().toISOString(), + ...normalized, + }; + await storage.updateData( + getDayKey(login, normalized.date), + current => [...(current ?? []), entry] + ); + return normalized.date; +} + +/** + * Uloží najednou více aktivit do jednoho dne (používá aplikace šablony). + * + * @param login přihlašovací jméno uživatele + * @param date den, do kterého se aktivity založí + * @param entries aktivity k uložení + */ +export async function addActivities(login: string, date: string, entries: ActivityEntry[]): Promise { + if (!entries.length) return; + await storage.updateData( + getDayKey(login, date), + current => [...(current ?? []), ...entries] + ); +} + +/** + * Upraví existující aktivitu. Změna data ji přesune do jiného dne. + * + * @param login přihlašovací jméno uživatele + * @param id identifikátor upravovaného záznamu + * @param input nová data aktivity + * @returns datum, ve kterém aktivita po úpravě je + */ +export async function updateActivity(login: string, id: unknown, input: unknown): Promise { + if (typeof id !== 'string' || !id.length) { + throw new BadRequestError("Nebyl předán identifikátor záznamu"); + } + const normalized = normalizeActivityInput(input); + const originalDate = await findActivityDate(login, id, normalized.date); + if (!originalDate) { + throw new NotFoundError("Aktivita nebyla nalezena"); + } + + let original: ActivityEntry | undefined; + await storage.updateData(getDayKey(login, originalDate), current => { + original = (current ?? []).find(entry => entry.id === id); + return (current ?? []).filter(entry => entry.id !== id); + }); + if (!original) { + throw new NotFoundError("Aktivita nebyla nalezena"); + } + + // Volitelná pole se přepisují včetně vymazání, proto se berou z normalizovaného vstupu + const updated: ActivityEntry = { + ...original, + ...normalized, + caloriesPer100Units: normalized.caloriesPer100Units, + calories: normalized.calories, + note: normalized.note, + updatedAt: new Date().toISOString(), + }; + await storage.updateData( + getDayKey(login, normalized.date), + current => [...(current ?? []), updated] + ); + return normalized.date; +} + +/** + * Smaže aktivitu. + * + * @param login přihlašovací jméno uživatele + * @param id identifikátor mazaného záznamu + * @param date datum záznamu (YYYY-MM-DD) + */ +export async function deleteActivity(login: string, id: unknown, date: unknown): Promise { + if (typeof id !== 'string' || !id.length) { + throw new BadRequestError("Nebyl předán identifikátor záznamu"); + } + if (!isValidIsoDate(date)) { + throw new BadRequestError("Nebylo předáno platné datum ve formátu YYYY-MM-DD"); + } + let found = false; + await storage.updateData(getDayKey(login, date), current => { + found = (current ?? []).some(entry => entry.id === id); + return (current ?? []).filter(entry => entry.id !== id); + }); + if (!found) { + throw new NotFoundError("Aktivita nebyla nalezena"); + } + return date; +} + +/** Najde datum, pod kterým je aktivita uložená. Úprava totiž může měnit i datum. */ +async function findActivityDate(login: string, id: string, expectedDate: string): Promise { + const expected = await storage.getData(getDayKey(login, expectedDate)); + if (expected?.some(entry => entry.id === id)) { + return expectedDate; + } + const prefix = `activities:${login}:`; + for (const key of await storage.listKeys(prefix)) { + if (!key.startsWith(prefix)) continue; + const entries = await storage.getData(key); + if (entries?.some(entry => entry.id === id)) { + return key.substring(prefix.length); + } + } + return undefined; +} diff --git a/server/src/auth.ts b/server/src/auth.ts new file mode 100644 index 0000000..ac6dab5 --- /dev/null +++ b/server/src/auth.ts @@ -0,0 +1,55 @@ +import jwt from 'jsonwebtoken'; + +/** + * Vygeneruje a vrátí podepsaný JWT token pro daný login. + * + * @param login přihlašovací jméno uživatele + * @param trusted příznak, zda se jedná o ověřeného uživatele + * @returns JWT token + */ +export function generateToken(login?: string, trusted?: boolean): string { + if (!process.env.JWT_SECRET) { + throw new Error("Není vyplněna proměnná prostředí JWT_SECRET"); + } + if (process.env.JWT_SECRET.length < 32) { + throw new Error("Proměnná prostředí JWT_SECRET musí být minimálně 32 znaků"); + } + if (!login || login.trim().length === 0) { + throw new Error("Nebyl předán login"); + } + const payload = { login, trusted: trusted || false, logoutUrl: process.env.LOGOUT_URL }; + return jwt.sign(payload, process.env.JWT_SECRET); +} + +/** + * Vrátí true, pokud je předaný JWT token platný. + * + * @param token JWT token + */ +export function verify(token: string): boolean { + if (!process.env.JWT_SECRET) { + throw new Error("Není vyplněna proměnná prostředí JWT_SECRET"); + } + try { + jwt.verify(token, process.env.JWT_SECRET); + return true; + } catch { + return false; + } +} + +/** + * Vrátí login z daného JWT tokenu, pokud je token platný. + * + * @param token JWT token + */ +export function getLogin(token?: string): string { + if (!process.env.JWT_SECRET) { + throw new Error("Není vyplněna proměnná prostředí JWT_SECRET"); + } + if (!token) { + throw new Error("Nebyl předán token"); + } + const payload: any = jwt.verify(token, process.env.JWT_SECRET); + return payload.login; +} diff --git a/server/src/calorieProvider.ts b/server/src/calorieProvider.ts new file mode 100644 index 0000000..ae1f198 --- /dev/null +++ b/server/src/calorieProvider.ts @@ -0,0 +1,106 @@ +import { CalorieSuggestion } from "../../types/gen/types.gen"; + +/** + * Externí zdroj energetických hodnot. + * + * Záměrně je to úzké rozhraní o jedné metodě — poskytovatelé bývají nespolehliví + * (viz {@link OpenFoodFactsProvider}) a je potřeba je umět vyměnit beze změny + * zbytku aplikace. Implementace nesmí vyhazovat výjimky: při nedostupnosti + * vrací `unavailable`, aby hledání jen přišlo o návrhy a nespadlo celé. + */ +export interface CalorieProvider { + /** Název poskytovatele, propisuje se do odpovědi API. */ + readonly name: string; + + /** + * Najde potraviny odpovídající názvu. + * + * @param query hledaný název + * @param limit maximální počet návrhů + */ + search(query: string, limit: number): Promise; +} + +/** Výsledek dotazu na poskytovatele. `unavailable` značí, že zdroj neodpověděl. */ +export type ProviderResult = + | { unavailable: false, suggestions: CalorieSuggestion[] } + | { unavailable: true }; + +/** Timeout dotazu na poskytovatele. Hledání kalorií nesmí blokovat zadávání jídla. */ +const REQUEST_TIMEOUT_MS = 6000; + +/** + * Open Food Facts jako zdroj energetických hodnot. + * + * Používá `search.openfoodfacts.org`, ne hlavní `world.openfoodfacts.org` — + * tamní `/cgi/search.pl` i `/api/v2/search` vrací kvůli zátěži 503, zatímco + * samostatná vyhledávací služba odpovídá. Data jsou CC, klíč není potřeba. + */ +export class OpenFoodFactsProvider implements CalorieProvider { + + readonly name = 'openfoodfacts'; + + private readonly baseUrl = 'https://search.openfoodfacts.org/search'; + + async search(query: string, limit: number): Promise { + // Poskytovatel vrací i produkty bez energetické hodnoty, tak si jich vyžádáme víc + const url = `${this.baseUrl}?q=${encodeURIComponent(query)}&page_size=${limit * 4}`; + try { + const response = await fetch(url, { + headers: { 'User-Agent': 'FoodTracer (osobni projekt)' }, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!response.ok) { + console.warn(`Open Food Facts odpověděl ${response.status}`); + return { unavailable: true }; + } + const body = await response.json() as { hits?: unknown[] }; + return { unavailable: false, suggestions: this.toSuggestions(body.hits ?? [], limit) }; + } catch (error) { + console.warn('Open Food Facts není dostupný:', (error as Error).message); + return { unavailable: true }; + } + } + + /** Vybere z odpovědi produkty s použitelnou energetickou hodnotou. */ + private toSuggestions(hits: unknown[], limit: number): CalorieSuggestion[] { + const suggestions: CalorieSuggestion[] = []; + for (const hit of hits) { + const product = hit as { product_name?: unknown, brands?: unknown, nutriments?: Record }; + const kcal = product.nutriments?.['energy-kcal_100g']; + const name = typeof product.product_name === 'string' ? product.product_name.trim() : ''; + if (!name.length || typeof kcal !== 'number' || !Number.isFinite(kcal) || kcal <= 0) { + continue; + } + suggestions.push({ + name, + caloriesPer100g: Math.round(kcal), + origin: this.name, + ...(Array.isArray(product.brands) && typeof product.brands[0] === 'string' + ? { brand: product.brands[0] } + : {}), + }); + if (suggestions.length >= limit) break; + } + return suggestions; + } +} + +/** Poskytovatel, který nic nenajde. Používá se, když je externí vyhledávání vypnuté. */ +export class NoopProvider implements CalorieProvider { + readonly name = 'none'; + + search(): Promise { + return Promise.resolve({ unavailable: false, suggestions: [] }); + } +} + +/** + * Vrátí poskytovatele dle konfigurace. + * `CALORIE_PROVIDER=none` vypne externí dotazy úplně (offline provoz, testy). + */ +export function getCalorieProvider(): CalorieProvider { + return process.env.CALORIE_PROVIDER?.toLowerCase() === 'none' + ? new NoopProvider() + : new OpenFoodFactsProvider(); +} diff --git a/server/src/calories.ts b/server/src/calories.ts new file mode 100644 index 0000000..ad9f590 --- /dev/null +++ b/server/src/calories.ts @@ -0,0 +1,146 @@ +import { CalorieSearchResult, CalorieSuggestion } from "../../types/gen/types.gen"; +import getStorage from "./storage"; +import { getCalorieProvider } from "./calorieProvider"; +import { BadRequestError } from "./utils"; + +const storage = getStorage(); + +/** Označení návrhů pocházejících z vlastní knihovny uživatele. */ +const LIBRARY_ORIGIN = 'library'; + +/** Kolik návrhů celkem vrátit. */ +const SUGGESTION_LIMIT = 8; + +/** Kolik z nich smí obsadit vlastní knihovna — zbytek zůstane na poskytovatele. */ +const LIBRARY_LIMIT = 3; + +/** + * Tabulka potravin na KalorickéTabulky.cz. + * + * Odkaz se uživateli jen otevře v novém panelu, nic se odtud nestahuje — veřejné + * API neexistuje, jejich robots.txt vyhledávání robotům zakazuje a smluvní + * podmínky omezují užití nad rámec zamýšleného účelu. + * + * Vede na tabulku potravin bez předvyplněného hledání: jejich vyhledávání běží + * v JavaScriptu a nedá se nastavit z URL (ověřeno — parametry v query stringu + * stránka ignoruje). Název jídla proto klient zkopíruje do schránky, aby ho + * uživatel jen vložil do jejich vyhledávacího pole. + */ +const EXTERNAL_SEARCH_BASE = 'https://www.kaloricketabulky.cz/tabulka-potravin'; + +/** Klíč úložiště s knihovnou energetických hodnot jednoho uživatele. */ +function getLibraryKey(login: string): string { + return `calorieLibrary:${login}`; +} + +/** + * Znormalizuje název jídla na klíč knihovny — bez diakritiky, malými písmeny + * a bez interpunkce, aby si "Kuřecí gyros" a "kureci gyros!" odpovídaly. + */ +export function normalizeName(name: string): string { + return name + .normalize('NFD').replace(/[\u0300-\u036f]/g, '') + .toLowerCase() + .replace(/[^a-z0-9]+/g, ' ') + .trim(); +} + +/** Jeden zapamatovaný záznam knihovny. */ +type LibraryEntry = { + /** Název tak, jak ho uživatel napsal naposledy */ + name: string; + caloriesPer100g: number; + /** Kolikrát už uživatel tuto hodnotu použil — určuje pořadí návrhů */ + uses: number; + updatedAt: string; +}; + +type Library = Record; + +/** + * Zapamatuje si energetickou hodnotu pod názvem jídla, aby ji šlo příště nabídnout. + * Volá se při uložení jídla, které má vyplněné caloriesPer100g. + * + * @param login přihlašovací jméno uživatele + * @param name název jídla + * @param caloriesPer100g energetická hodnota na 100 g v kcal + */ +export async function rememberCalories(login: string, name: string, caloriesPer100g: number): Promise { + const key = normalizeName(name); + if (!key.length || !Number.isFinite(caloriesPer100g) || caloriesPer100g <= 0) return; + + await storage.updateData(getLibraryKey(login), current => { + const library = current ?? {}; + const existing = library[key]; + return { + ...library, + [key]: { + name: name.trim(), + caloriesPer100g: Math.round(caloriesPer100g), + // Opakované použití stejné hodnoty ji posouvá výš mezi návrhy + uses: (existing?.uses ?? 0) + 1, + updatedAt: new Date().toISOString(), + }, + }; + }); +} + +/** Najde v knihovně záznamy, jejichž název odpovídá hledanému výrazu. */ +async function searchLibrary(login: string, query: string): Promise { + const library = await storage.getData(getLibraryKey(login)) ?? {}; + const needle = normalizeName(query); + if (!needle.length) return []; + + return Object.entries(library) + .filter(([key]) => key === needle || key.includes(needle) || needle.includes(key)) + // Přesná shoda první, pak nejpoužívanější + .sort(([keyA, a], [keyB, b]) => + Number(keyB === needle) - Number(keyA === needle) || b.uses - a.uses) + .slice(0, LIBRARY_LIMIT) + .map(([, entry]) => ({ + name: entry.name, + caloriesPer100g: entry.caloriesPer100g, + origin: LIBRARY_ORIGIN, + })); +} + +/** + * Najde návrhy energetické hodnoty pro název jídla. + * + * Nejdřív se hledá ve vlastní knihovně (dřívější zadání uživatele sedí na + * kantýnová jídla líp než veřejná databáze balených potravin), teprve pak se + * doplní návrhy od poskytovatele. Když je poskytovatel nedostupný, vrátí se + * jen knihovna a příznak `providerAvailable: false`. + * + * @param login přihlašovací jméno uživatele + * @param query hledaný název jídla + */ +export async function searchCalories(login: string, query: unknown): Promise { + if (typeof query !== 'string' || !query.trim().length) { + throw new BadRequestError("Nebyl předán hledaný název"); + } + const trimmed = query.trim(); + const provider = getCalorieProvider(); + + const fromLibrary = await searchLibrary(login, trimmed); + const result = await provider.search(trimmed, SUGGESTION_LIMIT - fromLibrary.length); + + const suggestions = [...fromLibrary]; + if (!result.unavailable) { + // Co už nabídla knihovna, nemá smysl opakovat z externího zdroje + const seen = new Set(fromLibrary.map(item => normalizeName(item.name))); + for (const suggestion of result.suggestions) { + if (seen.has(normalizeName(suggestion.name))) continue; + seen.add(normalizeName(suggestion.name)); + suggestions.push(suggestion); + } + } + + return { + query: trimmed, + providerAvailable: !result.unavailable, + providerName: provider.name, + suggestions, + externalSearchUrl: EXTERNAL_SEARCH_BASE, + }; +} diff --git a/server/src/dayOverview.ts b/server/src/dayOverview.ts new file mode 100644 index 0000000..ee1380b --- /dev/null +++ b/server/src/dayOverview.ts @@ -0,0 +1,48 @@ +import { DayOverview, EnergyBalance } from "../../types/gen/types.gen"; +import { getDay } from "./meals"; +import { getActivityDay } from "./activities"; +import { getSettings } from "./settings"; + +/** + * Spočítá energetickou bilanci dne. + * + * Výdej je součet klidového metabolismu a pohybu. Bez nastaveného klidového + * výdeje porovnává bilance jen jídlo proti pohybu — to není skutečný deficit, + * proto se to příznakem `hasBasal` propisuje do UI. + * + * @param intake přijaté kcal z jídla + * @param activityBurn spálené kcal pohybem + * @param basal klidový výdej v kcal za den, nebo nic + */ +export function buildEnergyBalance(intake: number, activityBurn: number, basal?: number): EnergyBalance { + const resolvedBasal = basal ?? 0; + const totalBurn = resolvedBasal + activityBurn; + return { + intake, + activityBurn, + basal: resolvedBasal, + totalBurn, + balance: intake - totalBurn, + hasBasal: !!basal, + }; +} + +/** + * Sestaví kompletní přehled dne — jídlo, pohyb a jejich bilanci. + * + * @param login přihlašovací jméno uživatele + * @param date datum ve formátu YYYY-MM-DD + */ +export async function getDayOverview(login: string, date: string): Promise { + const [meals, activities, settings] = await Promise.all([ + getDay(login, date), + getActivityDay(login, date), + getSettings(login), + ]); + return { + date, + meals, + activities, + energy: buildEnergyBalance(meals.totalCalories, activities.totalCalories, settings.basalCalories), + }; +} diff --git a/server/src/index.ts b/server/src/index.ts new file mode 100644 index 0000000..e80d9a3 --- /dev/null +++ b/server/src/index.ts @@ -0,0 +1,178 @@ +import express from "express"; +import cors from 'cors'; +import dotenv from 'dotenv'; +import path from 'path'; +import { generateToken, verify } from "./auth"; +import { BadRequestError, NotFoundError } from "./utils"; +import getStorage, { storageReady } from "./storage"; +import { shutdownRedisStorage } from "./storage/redis"; +import mealRoutes from "./routes/mealRoutes"; +import statsRoutes from "./routes/statsRoutes"; +import importRoutes from "./routes/importRoutes"; +import calorieRoutes from "./routes/calorieRoutes"; +import settingsRoutes from "./routes/settingsRoutes"; +import activityRoutes from "./routes/activityRoutes"; +import workoutRoutes from "./routes/workoutRoutes"; +import dayRoutes from "./routes/dayRoutes"; + +const ENVIRONMENT = process.env.NODE_ENV ?? 'production'; +dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) }); + +if (!process.env.JWT_SECRET) { + throw new Error("Není vyplněna proměnná prostředí JWT_SECRET"); +} + +export const app = express(); +const server = require("http").createServer(app); + +// Keep-alive delší než idle timeout reverzní proxy; headersTimeout musí být větší než keepAliveTimeout +server.keepAliveTimeout = 65_000; +server.headersTimeout = 66_000; +server.requestTimeout = 30_000; + +// Importovaný XLSX chodí v těle jako Base64, výchozí limit 100 kB by na měsíční přehled nestačil +app.use(express.json({ limit: '15mb' })); +app.use(cors({ origin: '*' })); + +const HTTP_REMOTE_USER_ENABLED = process.env.HTTP_REMOTE_USER_ENABLED === 'true'; +const HTTP_REMOTE_USER_HEADER_NAME = process.env.HTTP_REMOTE_USER_HEADER_NAME ?? 'remote-user'; +if (HTTP_REMOTE_USER_ENABLED) { + if (!process.env.HTTP_REMOTE_TRUSTED_IPS) { + throw new Error('Je zapnutý login z hlaviček, ale není nastaven rozsah adres ze kterých hlavička může přijít.'); + } + app.set('trust proxy', process.env.HTTP_REMOTE_TRUSTED_IPS.split(',').map(ip => ip.trim())); + console.log('Zapnutý login přes hlavičky z proxy.'); +} + +// ─── Shutdown ───────────────────────────────────────────────────────────────── + +let shuttingDown = false; + +async function shutdown(signal: string) { + if (shuttingDown) return; + shuttingDown = true; + console.log(`${signal} received — initiating graceful shutdown`); + + setTimeout(() => { + console.error('Graceful shutdown timed out, forcing exit'); + process.exit(1); + }, 25_000).unref(); + + (server as any).closeIdleConnections?.(); + await new Promise(resolve => server.close(() => resolve())); + + if (process.env.STORAGE?.toLowerCase() === 'redis') { + await shutdownRedisStorage(); + } + console.log('Graceful shutdown complete'); + process.exit(0); +} + +process.on('SIGTERM', () => shutdown('SIGTERM')); +process.on('SIGINT', () => shutdown('SIGINT')); + +// ─── Routes bez autentizace ─────────────────────────────────────────────────── + +/** Liveness probe — levná, bez externích závislostí. */ +app.get("/api/health", (_req, res) => { + res.status(200).json({ ok: true }); +}); + +/** Readiness probe — ověří dostupnost úložiště a odmítá provoz při shutdownu. */ +app.get("/api/health/ready", async (_req, res) => { + if (shuttingDown) { + res.status(503).json({ ok: false, reason: 'shutting down' }); + return; + } + const healthy = await getStorage().healthCheck?.() ?? true; + if (!healthy) { + res.status(503).json({ ok: false, reason: 'storage unavailable' }); + return; + } + res.status(200).json({ ok: true }); +}); + +/** Veřejná runtime konfigurace pro klienta. */ +app.get("/api/config", (_req, res) => { + res.status(200).json({ + sentry: { + dsn: process.env.SENTRY_CLIENT_DSN ?? process.env.SENTRY_DSN ?? null, + environment: ENVIRONMENT, + }, + }); +}); + +app.post("/api/login", (req, res, next) => { + try { + if (HTTP_REMOTE_USER_ENABLED) { + const remoteUser = req.header(HTTP_REMOTE_USER_HEADER_NAME); + if (!remoteUser?.length) { + throw new Error("Je zapnuto přihlášení přes hlavičky, ale hlavička nedorazila"); + } + res.status(200).json(generateToken(Buffer.from(remoteUser, 'latin1').toString(), true)); + return; + } + if (!req.body?.login || req.body.login.trim().length === 0) { + throw new BadRequestError("Nebyl předán login"); + } + res.status(200).json(generateToken(req.body.login, false)); + } catch (e) { next(e); } +}); + +// ─── Autentizace ────────────────────────────────────────────────────────────── + +app.use("/api/", (req, res, next) => { + if (!req.headers.authorization) { + res.status(401).json({ error: 'Nebyl předán autentizační token' }); + return; + } + const token = req.headers.authorization.split(' ')[1]; + if (!verify(token)) { + res.status(403).json({ error: 'Neplatný autentizační token' }); + return; + } + next(); +}); + +// ─── Routes vyžadující autentizaci ──────────────────────────────────────────── + +app.use("/api/meals", mealRoutes); +app.use("/api/stats", statsRoutes); +app.use("/api/import", importRoutes); +app.use("/api/calories", calorieRoutes); +app.use("/api/settings", settingsRoutes); +app.use("/api/activities", activityRoutes); +app.use("/api/workouts", workoutRoutes); +app.use("/api/day", dayRoutes); + +app.use(express.static(path.join(process.cwd(), 'public'))); +app.get('*splat', (_req, res) => { + res.sendFile(path.join(process.cwd(), 'public', 'index.html')); +}); + +// Error handling middleware +app.use((err: any, _req: any, res: any, next: any) => { + if (err instanceof BadRequestError) { + res.status(400).send({ error: err.message }); + } else if (err instanceof NotFoundError) { + res.status(404).send({ error: err.message }); + } else { + console.error(err); + res.status(500).send({ error: err.message }); + } + next(); +}); + +// ─── Bootstrap ──────────────────────────────────────────────────────────────── + +const PORT = process.env.PORT ?? 3001; +const HOST = process.env.HOST ?? '0.0.0.0'; + +// Testy si aplikaci importují a volají přes supertest, server v nich nespouštíme +if (require.main === module) { + storageReady.then(() => { + server.listen(PORT, () => { + console.log(`Server listening on ${HOST}, port ${PORT}`); + }); + }); +} diff --git a/server/src/luncherImport.ts b/server/src/luncherImport.ts new file mode 100644 index 0000000..4562189 --- /dev/null +++ b/server/src/luncherImport.ts @@ -0,0 +1,434 @@ +import ExcelJS from 'exceljs'; +import { randomUUID } from 'crypto'; +import { ImportResult, MealEntry, MealType } from "../../types/gen/types.gen"; +import { addEntries, getExistingImportKeys, isMealType } from "./meals"; +import { BadRequestError, isValidIsoDate } from "./utils"; + +/** Označení zdroje, kterým se importované záznamy značí. */ +export const IMPORT_SOURCE = 'luncher'; + +/** Podporované formáty importu (shodné s formáty exportu Luncheru). */ +export const IMPORT_FORMATS = ['xlsx', 'csv', 'json'] as const; + +export type ImportFormat = typeof IMPORT_FORMATS[number]; + +/** Název listu s jednotlivými záznamy v XLSX exportu. Druhý list ("Souhrn") se ignoruje. */ +const DETAIL_SHEET_NAME = 'Přehled'; + +/** Oddělovač polí v CSV exportu — Luncher generuje CSV pro český Excel. */ +const CSV_SEPARATOR = ';'; + +/** + * Mapování hlaviček sloupců exportu na klíče. Sloupce se hledají podle názvu, + * ne podle pozice, aby import přežil případné přeuspořádání exportu. + */ +const COLUMN_HEADERS: Record = { + 'datum': 'date', + 'den': 'dayOfWeek', + 'typ': 'type', + 'vybrané jídlo': 'food', + 'poznámka': 'note', + 'objednávka': 'store', + 'objednával': 'orderedBy', + 'částka': 'amount', +}; + +/** + * Typy záznamů, které nejsou podnikem — jde o stav volby, ne o místo, kde se jedlo. + * Odpovídají volbám OBJEDNAVAM / ROZHODUJI / NEOBEDVAM a objednávce bez volby + * v exportu Luncheru. Jako zdroj jídla by nedávaly smysl, tak se nepoužijí. + */ +const NON_PLACE_TYPES = new Set([ + 'Budu objednávat', + 'Rozhoduji se', + 'Mám vlastní/neobědvám', + 'Objednávka', +]); + +/** Jeden řádek přehledu z Luncheru, převedený do jednotné podoby. */ +type LuncherRow = { + /** Datum ve formátu YYYY-MM-DD */ + date: string; + dayOfWeek?: string; + /** Typ záznamu — název podniku/volby, nebo "Objednávka" */ + type: string; + food?: string; + note?: string; + store?: string; + orderedBy?: string; + /** Částka v haléřích */ + amount?: number; +}; + +/** Výsledek zpracování souboru — řádky plus upozornění na to, co se nepovedlo. */ +type ParsedFile = { rows: LuncherRow[], warnings: string[] }; + +/** + * Rozpozná formát souboru podle přípony. + * + * @param fileName název souboru + */ +export function detectFormat(fileName: string): ImportFormat { + const extension = fileName.toLowerCase().split('.').pop(); + if (extension && (IMPORT_FORMATS as readonly string[]).includes(extension)) { + return extension as ImportFormat; + } + throw new BadRequestError(`Nepodporovaný formát souboru '${fileName}'. Podporované jsou .xlsx, .csv a .json.`); +} + +/** Ořízne text a prázdnou hodnotu vrátí jako undefined. */ +function text(value: unknown): string | undefined { + if (value == null) return undefined; + const trimmed = String(value).trim(); + return trimmed.length ? trimmed : undefined; +} + +/** + * Převede částku v korunách na haléře. + * Přijímá i český zápis s desetinnou čárkou a mezerami mezi řády ("1 234,50 Kč"). + */ +function parseAmount(value: unknown): number | undefined { + if (value == null || value === '') return undefined; + if (typeof value === 'number') { + return Number.isFinite(value) ? Math.round(value * 100) : undefined; + } + const cleaned = String(value) + .replace(/\s| /g, '') // mezery i nezlomitelné mezery mezi řády + .replace(/Kč/gi, '') + .replace(',', '.'); + if (!cleaned.length) return undefined; + const parsed = Number(cleaned); + return Number.isFinite(parsed) ? Math.round(parsed * 100) : undefined; +} + +/** + * Převede datum z exportu na ISO formát YYYY-MM-DD. + * Zvládne ISO řetězec (JSON export), český zápis DD.MM.YYYY (CSV) i Date (XLSX). + */ +function parseDate(value: unknown): string | undefined { + if (value == null) return undefined; + if (value instanceof Date) { + // XLSX ukládá datum v UTC (viz export Luncheru), proto se čte v UTC + const year = value.getUTCFullYear(); + const month = String(value.getUTCMonth() + 1).padStart(2, '0'); + const day = String(value.getUTCDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } + const raw = String(value).trim(); + if (isValidIsoDate(raw)) { + return raw; + } + const czech = /^(\d{1,2})\.\s*(\d{1,2})\.\s*(\d{4})$/.exec(raw); + if (czech) { + const [, day, month, year] = czech; + const iso = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; + return isValidIsoDate(iso) ? iso : undefined; + } + return undefined; +} + +/** + * Rozdělí CSV na buňky. Respektuje uvozovky i zdvojené uvozovky uvnitř hodnoty, + * takže si poradí i s poznámkou obsahující oddělovač nebo konec řádku. + */ +function parseCsv(content: string): string[][] { + const rows: string[][] = []; + let row: string[] = []; + let cell = ''; + let inQuotes = false; + + for (let index = 0; index < content.length; index++) { + const char = content[index]; + if (inQuotes) { + if (char === '"') { + if (content[index + 1] === '"') { + cell += '"'; + index++; + } else { + inQuotes = false; + } + } else { + cell += char; + } + continue; + } + if (char === '"') { + inQuotes = true; + } else if (char === CSV_SEPARATOR) { + row.push(cell); + cell = ''; + } else if (char === '\n') { + row.push(cell); + rows.push(row); + row = []; + cell = ''; + } else if (char !== '\r') { + cell += char; + } + } + if (cell.length || row.length) { + row.push(cell); + rows.push(row); + } + return rows.filter(current => current.some(value => value.trim().length)); +} + +/** + * Sestaví z hlavičkového řádku mapu index sloupce -> klíč řádku. + * Nerozpoznané sloupce se ignorují. + */ +function mapHeaders(headers: unknown[]): Map { + const mapping = new Map(); + headers.forEach((header, index) => { + const key = COLUMN_HEADERS[String(header ?? '').trim().toLowerCase()]; + if (key) { + mapping.set(index, key); + } + }); + return mapping; +} + +/** Poskládá řádek přehledu z buněk dle mapování sloupců. */ +function buildRow(cells: unknown[], mapping: Map): LuncherRow | undefined { + const raw: Record = {}; + for (const [index, key] of mapping) { + raw[key] = cells[index]; + } + const date = parseDate(raw.date); + if (!date) return undefined; + return { + date, + dayOfWeek: text(raw.dayOfWeek), + type: text(raw.type) ?? '', + food: text(raw.food), + note: text(raw.note), + store: text(raw.store), + orderedBy: text(raw.orderedBy), + amount: parseAmount(raw.amount), + }; +} + +/** Zpracuje CSV podobu exportu. */ +function parseCsvExport(content: Buffer): ParsedFile { + // Luncher píše CSV s BOM kvůli Excelu, JS ho musí odstranit ručně + const csv = content.toString('utf-8').replace(/^/, ''); + const table = parseCsv(csv); + if (!table.length) { + throw new BadRequestError("CSV soubor je prázdný"); + } + const mapping = mapHeaders(table[0]); + if (!mapping.size) { + throw new BadRequestError("V CSV se nepodařilo najít očekávané sloupce přehledu z Luncheru"); + } + const rows: LuncherRow[] = []; + const warnings: string[] = []; + table.slice(1).forEach((cells, index) => { + const row = buildRow(cells, mapping); + if (row) { + rows.push(row); + } else { + warnings.push(`Řádek ${index + 2} přeskočen — nepodařilo se přečíst datum`); + } + }); + return { rows, warnings }; +} + +/** Zpracuje XLSX podobu exportu. */ +async function parseXlsxExport(content: Buffer): Promise { + const workbook = new ExcelJS.Workbook(); + try { + await workbook.xlsx.load(content as unknown as ArrayBuffer); + } catch { + throw new BadRequestError("Soubor se nepodařilo přečíst jako XLSX"); + } + const sheet = workbook.getWorksheet(DETAIL_SHEET_NAME) ?? workbook.worksheets[0]; + if (!sheet) { + throw new BadRequestError("XLSX soubor neobsahuje žádný list"); + } + + /** Vrátí hodnoty buněk řádku v poli indexovaném od nuly. */ + const cellsOf = (rowNumber: number): unknown[] => { + const row = sheet.getRow(rowNumber); + const values: unknown[] = []; + for (let column = 1; column <= sheet.columnCount; column++) { + const cell = row.getCell(column); + // Vzorce vracejí objekt { formula, result } — zajímá nás spočtená hodnota + const value = cell.value; + values.push(value && typeof value === 'object' && 'result' in value ? value.result : value); + } + return values; + }; + + const mapping = mapHeaders(cellsOf(1)); + if (!mapping.size) { + throw new BadRequestError("V XLSX se nepodařilo najít očekávané sloupce přehledu z Luncheru"); + } + const rows: LuncherRow[] = []; + const warnings: string[] = []; + for (let rowNumber = 2; rowNumber <= sheet.rowCount; rowNumber++) { + const cells = cellsOf(rowNumber); + if (cells.every(value => value == null || String(value).trim() === '')) continue; + const row = buildRow(cells, mapping); + if (row) { + rows.push(row); + } else { + warnings.push(`Řádek ${rowNumber} přeskočen — nepodařilo se přečíst datum`); + } + } + return { rows, warnings }; +} + +/** Zpracuje JSON podobu exportu (schéma UserExport z Luncheru). */ +function parseJsonExport(content: Buffer): ParsedFile { + let parsed: any; + try { + parsed = JSON.parse(content.toString('utf-8')); + } catch { + throw new BadRequestError("Soubor se nepodařilo přečíst jako JSON"); + } + const rawRows = Array.isArray(parsed) ? parsed : parsed?.rows; + if (!Array.isArray(rawRows)) { + throw new BadRequestError("JSON neobsahuje pole 'rows' s řádky přehledu"); + } + const rows: LuncherRow[] = []; + const warnings: string[] = []; + rawRows.forEach((raw: any, index: number) => { + const date = parseDate(raw?.date); + if (!date) { + warnings.push(`Záznam ${index + 1} přeskočen — nepodařilo se přečíst datum`); + return; + } + rows.push({ + date, + dayOfWeek: text(raw.dayOfWeek), + type: text(raw.type) ?? '', + food: text(raw.food), + note: text(raw.note), + store: text(raw.store), + orderedBy: text(raw.orderedBy), + amount: parseAmount(raw.amount), + }); + }); + return { rows, warnings }; +} + +/** + * Sestaví klíč pro rozpoznání duplicit. + * + * Součástí je i pořadí řádku v rámci dne — v jednom dni může být víc stejných + * záznamů (dvě totožné objednávky) a ty se nesmí navzájem odfiltrovat. + * Export Luncheru řadí řádky deterministicky, takže při opakovaném exportu + * téhož měsíce vyjde stejné pořadí. + */ +function buildImportKey(row: LuncherRow, indexWithinDay: number): string { + return [ + IMPORT_SOURCE, + row.date, + indexWithinDay, + row.type, + row.food ?? '', + row.amount ?? '', + ].join('|'); +} + +/** + * Převede řádek přehledu na záznam o jídle. + * + * Název se bere z vybraného jídla. U voleb jako "Budu objednávat" nebo + * "Rozhoduji se" je ale sloupec s jídlem prázdný a co uživatel reálně jedl stojí + * v poznámce ("Chefie - Těstovinový salát"), tak se použije ta. Teprve když není + * ani poznámka, zbude typ záznamu — uživatel si zvolil podnik, ale jídlo neuvedl. + * + * Zdrojem je obchod objednávky, jinak podnik. Stav volby se jako zdroj nepoužije. + */ +function toMealEntry(row: LuncherRow, mealType: MealType, importKey: string): MealEntry { + const noteAsName = !row.food && !!row.note; + const name = row.food ?? row.note ?? row.type; + // Poznámku, ze které se stal název, už do poznámky nekopírujeme + const remainingNote = noteAsName ? undefined : row.note; + const note = row.orderedBy + ? [remainingNote, `objednal(a): ${row.orderedBy}`].filter(Boolean).join(' | ') + : remainingNote; + return { + id: randomUUID(), + date: row.date, + mealType, + name, + source: row.store ?? (NON_PLACE_TYPES.has(row.type) ? undefined : row.type), + price: row.amount, + note, + importSource: IMPORT_SOURCE, + importKey, + createdAt: new Date().toISOString(), + }; +} + +/** + * Naimportuje přehled vyexportovaný z Luncheru do jídel uživatele. + * + * Export je typicky za celý měsíc — záznamy se rozdělí do dnů, ke kterým patří. + * Řádky, které už z dřívějšího importu existují, se přeskočí, takže opakovaný + * import stejného měsíce data nezduplikuje. + * + * @param login přihlašovací jméno uživatele + * @param fileName název souboru (určuje formát) + * @param content obsah souboru + * @param defaultMealType typ jídla pro importované řádky, výchozí je oběd + * @param dryRun pokud true, výsledek se jen spočítá a nic se neuloží + */ +export async function importLuncherExport( + login: string, + fileName: string, + content: Buffer, + defaultMealType?: unknown, + dryRun = false, +): Promise { + const format = detectFormat(fileName); + if (defaultMealType != null && !isMealType(defaultMealType)) { + throw new BadRequestError("Nebyl předán platný typ jídla"); + } + // Luncher řeší výběr obědů, takže bez upřesnění zakládáme importy jako oběd + const mealType: MealType = isMealType(defaultMealType) ? defaultMealType : MealType.OBED; + + let parsed: ParsedFile; + if (format === 'csv') { + parsed = parseCsvExport(content); + } else if (format === 'json') { + parsed = parseJsonExport(content); + } else { + parsed = await parseXlsxExport(content); + } + + const dates = Array.from(new Set(parsed.rows.map(row => row.date))); + const existingKeys = await getExistingImportKeys(login, dates); + + const perDayIndex = new Map(); + const entries: MealEntry[] = []; + let skipped = 0; + for (const row of parsed.rows) { + const indexWithinDay = perDayIndex.get(row.date) ?? 0; + perDayIndex.set(row.date, indexWithinDay + 1); + const importKey = buildImportKey(row, indexWithinDay); + if (existingKeys.has(importKey)) { + skipped++; + continue; + } + entries.push(toMealEntry(row, mealType, importKey)); + } + + const days = dryRun + ? Array.from(new Set(entries.map(entry => entry.date))).sort() + : await addEntries(login, entries); + + return { + dryRun, + format, + rowCount: parsed.rows.length, + imported: entries.length, + skipped, + days, + totalPrice: entries.reduce((sum, entry) => sum + (entry.price ?? 0), 0), + entries, + warnings: parsed.warnings, + }; +} diff --git a/server/src/meals.ts b/server/src/meals.ts new file mode 100644 index 0000000..b1c71d0 --- /dev/null +++ b/server/src/meals.ts @@ -0,0 +1,337 @@ +import { randomUUID } from 'crypto'; +import { DayRecord, MealEntry, MealType } from "../../types/gen/types.gen"; +import getStorage from "./storage"; +import { BadRequestError, NotFoundError, isValidIsoDate, listDatesInRange } from "./utils"; +import { rememberCalories } from "./calories"; + +const storage = getStorage(); + +/** Maximální délka textových polí, aby se do úložiště nedostaly nesmyslně velké hodnoty. */ +const MAX_TEXT_LENGTH = 500; + +/** + * Pořadí typů jídel v rámci dne. Podle něj se řadí záznamy dne, + * aby přehled odpovídal skutečnému průběhu dne, ne pořadí zadání. + */ +const MEAL_TYPE_ORDER: MealType[] = [ + MealType.SNIDANE, + MealType.DOPOLEDNI_SVACINA, + MealType.OBED, + MealType.ODPOLEDNI_SVACINA, + MealType.VECERE, + MealType.JINE, +]; + +/** Vrátí true, pokud je hodnota podporovaným typem jídla. */ +export function isMealType(value: unknown): value is MealType { + return typeof value === 'string' && (MEAL_TYPE_ORDER as string[]).includes(value); +} + +/** Sestaví klíč úložiště pro jeden den jednoho uživatele. */ +function getDayKey(login: string, date: string): string { + return `meals:${login}:${date}`; +} + +/** Seřadí jídla dne dle typu (snídaně -> večeře) a při shodě dle času vzniku. */ +function sortEntries(entries: MealEntry[]): MealEntry[] { + return [...entries].sort((a, b) => { + const typeDiff = MEAL_TYPE_ORDER.indexOf(a.mealType) - MEAL_TYPE_ORDER.indexOf(b.mealType); + return typeDiff !== 0 ? typeDiff : a.createdAt.localeCompare(b.createdAt); + }); +} + +/** Sestaví přehled dne včetně součtů z předaných záznamů. */ +export function buildDayRecord(date: string, entries: MealEntry[]): DayRecord { + const sorted = sortEntries(entries); + return { + date, + entries: sorted, + totalPrice: sorted.reduce((sum, entry) => sum + (entry.price ?? 0), 0), + totalCalories: sorted.reduce((sum, entry) => sum + (entry.calories ?? 0), 0), + }; +} + +/** + * Vrátí jídla uživatele za jeden den. + * + * @param login přihlašovací jméno uživatele + * @param date datum ve formátu YYYY-MM-DD + */ +export async function getDay(login: string, date: string): Promise { + const entries = await storage.getData(getDayKey(login, date)); + return buildDayRecord(date, entries ?? []); +} + +/** + * Vrátí dny uživatele v rozsahu, které obsahují alespoň jeden záznam. + * + * @param login přihlašovací jméno uživatele + * @param from první den rozsahu (včetně), YYYY-MM-DD + * @param to poslední den rozsahu (včetně), YYYY-MM-DD + */ +export async function getRange(login: string, from: string, to: string): Promise { + if (from > to) { + throw new BadRequestError("Začátek rozsahu je až za jeho koncem"); + } + const days = await Promise.all( + listDatesInRange(from, to).map(date => getDay(login, date)) + ); + return days.filter(day => day.entries.length > 0); +} + +/** Ořízne a znormalizuje volitelný textový vstup. Prázdný text vrací jako undefined. */ +function normalizeText(value: unknown, name: string): string | undefined { + if (value == null) return undefined; + if (typeof value !== 'string') { + throw new BadRequestError(`Pole '${name}' musí být text`); + } + const trimmed = value.trim(); + if (!trimmed.length) return undefined; + if (trimmed.length > MAX_TEXT_LENGTH) { + throw new BadRequestError(`Pole '${name}' může mít nejvýše ${MAX_TEXT_LENGTH} znaků`); + } + return trimmed; +} + +/** Ověří volitelnou nezápornou celočíselnou hodnotu (cena v haléřích, kalorie). */ +function normalizeNumber(value: unknown, name: string): number | undefined { + if (value == null) return undefined; + if (typeof value !== 'number' || !Number.isFinite(value)) { + throw new BadRequestError(`Pole '${name}' musí být číslo`); + } + const rounded = Math.round(value); + if (rounded < 0) { + throw new BadRequestError(`Pole '${name}' nesmí být záporné`); + } + return rounded; +} + +/** + * Znormalizovaný vstup jídla. Oproti {@link MealInput} jsou datum, typ i název po + * validaci vždy vyplněné a nevyplněná volitelná pole jsou `undefined` místo `null`, + * takže je lze rovnou rozprostřít do {@link MealEntry}. + */ +export type NormalizedMealInput = { + date: string; + mealType: MealType; + name: string; + source?: string; + note?: string; + price?: number; + calories?: number; + weight?: number; + pricePer100g?: number; + caloriesPer100g?: number; +}; + +/** + * Dopočte gramáž a kalorie z jednotkových hodnot. + * + * Podniky jako TechTower účtují podle váhy, takže z ceny a sazby za 100 g jde + * gramáž odvodit. Ručně zadaná gramáž má přednost — porce se dá zvážit přesněji, + * než kolik řekne cena, a u položek s jinou sazbou (salát) uživatel sazbu přepíše. + * Kalorie se stejně tak dopočtou z gramáže, pokud je známá energie na 100 g. + */ +export function deriveAmounts(input: NormalizedMealInput): NormalizedMealInput { + const weight = input.weight ?? ( + input.price != null && input.pricePer100g + ? Math.round(input.price / input.pricePer100g * 100) + : undefined + ); + const calories = weight != null && input.caloriesPer100g + ? Math.round(weight / 100 * input.caloriesPer100g) + : input.calories; + return { ...input, weight, calories }; +} + +/** Ověří a znormalizuje vstupní data jídla. */ +export function normalizeInput(input: unknown): NormalizedMealInput { + const value = (input ?? {}) as Record; + if (!isValidIsoDate(value.date)) { + throw new BadRequestError("Nebylo předáno platné datum ve formátu YYYY-MM-DD"); + } + if (!isMealType(value.mealType)) { + throw new BadRequestError("Nebyl předán platný typ jídla"); + } + const name = normalizeText(value.name, 'name'); + if (!name) { + throw new BadRequestError("Nebyl předán název jídla"); + } + return deriveAmounts({ + date: value.date, + mealType: value.mealType, + name, + source: normalizeText(value.source, 'source'), + note: normalizeText(value.note, 'note'), + price: normalizeNumber(value.price, 'price'), + calories: normalizeNumber(value.calories, 'calories'), + weight: normalizeNumber(value.weight, 'weight'), + pricePer100g: normalizeNumber(value.pricePer100g, 'pricePer100g'), + caloriesPer100g: normalizeNumber(value.caloriesPer100g, 'caloriesPer100g'), + }); +} + +/** + * Přidá jídlo do zvoleného dne a vrátí aktualizovaný přehled dne. + * + * @param login přihlašovací jméno uživatele + * @param input data jídla + */ +export async function addMeal(login: string, input: unknown): Promise { + const normalized = normalizeInput(input); + const entry: MealEntry = { + id: randomUUID(), + createdAt: new Date().toISOString(), + ...normalized, + }; + const entries = await storage.updateData( + getDayKey(login, normalized.date), + current => [...(current ?? []), entry] + ); + if (normalized.caloriesPer100g) { + await rememberCalories(login, normalized.name, normalized.caloriesPer100g); + } + return buildDayRecord(normalized.date, entries); +} + +/** + * Uloží najednou více záznamů (používá import). Záznamy se rozdělí dle data. + * Vrátí dny, kterých se zápis dotkl. + * + * @param login přihlašovací jméno uživatele + * @param entries záznamy k uložení + */ +export async function addEntries(login: string, entries: MealEntry[]): Promise { + const byDate = new Map(); + for (const entry of entries) { + const forDate = byDate.get(entry.date) ?? []; + forDate.push(entry); + byDate.set(entry.date, forDate); + } + for (const [date, forDate] of byDate) { + await storage.updateData( + getDayKey(login, date), + current => [...(current ?? []), ...forDate] + ); + } + return Array.from(byDate.keys()).sort(); +} + +/** + * Upraví existující záznam o jídle a vrátí aktualizovaný přehled dne. + * + * Změna data znamená přesun mezi dny — záznam se ze starého dne odebere + * a do nového přidá. Vrací se vždy přehled cílového (nového) dne. + * + * @param login přihlašovací jméno uživatele + * @param id identifikátor upravovaného záznamu + * @param input nová data jídla + */ +export async function updateMeal(login: string, id: unknown, input: unknown): Promise { + if (typeof id !== 'string' || !id.length) { + throw new BadRequestError("Nebyl předán identifikátor záznamu"); + } + const normalized = normalizeInput(input); + const originalDate = await findEntryDate(login, id, normalized.date); + if (!originalDate) { + throw new NotFoundError("Záznam o jídle nebyl nalezen"); + } + + let original: MealEntry | undefined; + await storage.updateData(getDayKey(login, originalDate), current => { + original = (current ?? []).find(entry => entry.id === id); + return (current ?? []).filter(entry => entry.id !== id); + }); + if (!original) { + throw new NotFoundError("Záznam o jídle nebyl nalezen"); + } + + // Cena, kalorie, zdroj i poznámka se přepisují včetně vymazání na prázdno, + // proto se berou z normalizovaného vstupu a ne z původního záznamu. + const updated: MealEntry = { + ...original, + ...normalized, + price: normalized.price, + calories: normalized.calories, + source: normalized.source, + note: normalized.note, + weight: normalized.weight, + pricePer100g: normalized.pricePer100g, + caloriesPer100g: normalized.caloriesPer100g, + updatedAt: new Date().toISOString(), + }; + const entries = await storage.updateData( + getDayKey(login, normalized.date), + current => [...(current ?? []), updated] + ); + if (normalized.caloriesPer100g) { + await rememberCalories(login, normalized.name, normalized.caloriesPer100g); + } + return buildDayRecord(normalized.date, entries); +} + +/** + * Smaže záznam o jídle a vrátí aktualizovaný přehled dne. + * + * @param login přihlašovací jméno uživatele + * @param id identifikátor mazaného záznamu + * @param date datum záznamu (YYYY-MM-DD) + */ +export async function deleteMeal(login: string, id: unknown, date: unknown): Promise { + if (typeof id !== 'string' || !id.length) { + throw new BadRequestError("Nebyl předán identifikátor záznamu"); + } + if (!isValidIsoDate(date)) { + throw new BadRequestError("Nebylo předáno platné datum ve formátu YYYY-MM-DD"); + } + let found = false; + const entries = await storage.updateData(getDayKey(login, date), current => { + found = (current ?? []).some(entry => entry.id === id); + return (current ?? []).filter(entry => entry.id !== id); + }); + if (!found) { + throw new NotFoundError("Záznam o jídle nebyl nalezen"); + } + return buildDayRecord(date, entries); +} + +/** + * Najde datum, pod kterým je záznam uložený. + * Nejdřív zkusí očekávaný den, teprve pak prohledá ostatní klíče uživatele — + * úprava záznamu totiž může měnit i datum. + */ +async function findEntryDate(login: string, id: string, expectedDate: string): Promise { + const expected = await storage.getData(getDayKey(login, expectedDate)); + if (expected?.some(entry => entry.id === id)) { + return expectedDate; + } + const prefix = `meals:${login}:`; + for (const key of await storage.listKeys(prefix)) { + if (!key.startsWith(prefix)) continue; + const entries = await storage.getData(key); + if (entries?.some(entry => entry.id === id)) { + return key.substring(prefix.length); + } + } + return undefined; +} + +/** + * Vrátí klíče importovaných záznamů uživatele v předaných dnech. + * Slouží k rozpoznání duplicit při opakovaném importu stejného měsíce. + * + * @param login přihlašovací jméno uživatele + * @param dates dny, ve kterých se má hledat + */ +export async function getExistingImportKeys(login: string, dates: string[]): Promise> { + const keys = new Set(); + for (const date of dates) { + const entries = await storage.getData(getDayKey(login, date)); + for (const entry of entries ?? []) { + if (entry.importKey) { + keys.add(entry.importKey); + } + } + } + return keys; +} diff --git a/server/src/routes/activityRoutes.ts b/server/src/routes/activityRoutes.ts new file mode 100644 index 0000000..4950d72 --- /dev/null +++ b/server/src/routes/activityRoutes.ts @@ -0,0 +1,38 @@ +import express, { Request, Response, NextFunction } from "express"; +import { addActivity, deleteActivity, updateActivity } from "../activities"; +import { getDayOverview } from "../dayOverview"; +import { getLogin } from "../auth"; +import { parseToken } from "../utils"; + +const router = express.Router(); + +/** Přidá pohybovou aktivitu a vrátí aktualizovaný přehled dne. */ +router.post("/add", async (req: Request, res: Response, next: NextFunction) => { + try { + const login = getLogin(parseToken(req)); + const date = await addActivity(login, req.body); + res.status(200).json(await getDayOverview(login, date)); + } catch (e) { next(e); } +}); + +/** Upraví existující aktivitu. */ +router.post("/update", async (req: Request, res: Response, next: NextFunction) => { + try { + const login = getLogin(parseToken(req)); + const { id, ...input } = req.body ?? {}; + const date = await updateActivity(login, id, input); + res.status(200).json(await getDayOverview(login, date)); + } catch (e) { next(e); } +}); + +/** Smaže aktivitu. */ +router.post("/delete", async (req: Request, res: Response, next: NextFunction) => { + try { + const login = getLogin(parseToken(req)); + const { id, date } = req.body ?? {}; + const day = await deleteActivity(login, id, date); + res.status(200).json(await getDayOverview(login, day)); + } catch (e) { next(e); } +}); + +export default router; diff --git a/server/src/routes/calorieRoutes.ts b/server/src/routes/calorieRoutes.ts new file mode 100644 index 0000000..fcefd87 --- /dev/null +++ b/server/src/routes/calorieRoutes.ts @@ -0,0 +1,16 @@ +import express, { Request, Response, NextFunction } from "express"; +import { searchCalories } from "../calories"; +import { getLogin } from "../auth"; +import { parseToken } from "../utils"; + +const router = express.Router(); + +/** Najde návrhy energetické hodnoty pro název jídla. */ +router.get("/search", async (req: Request, res: Response, next: NextFunction) => { + try { + const login = getLogin(parseToken(req)); + res.status(200).json(await searchCalories(login, req.query.q)); + } catch (e) { next(e); } +}); + +export default router; diff --git a/server/src/routes/dayRoutes.ts b/server/src/routes/dayRoutes.ts new file mode 100644 index 0000000..97125dc --- /dev/null +++ b/server/src/routes/dayRoutes.ts @@ -0,0 +1,19 @@ +import express, { Request, Response, NextFunction } from "express"; +import { getDayOverview } from "../dayOverview"; +import { getLogin } from "../auth"; +import { formatDate, getToday, parseToken, requireIsoDate } from "../utils"; + +const router = express.Router(); + +/** Vrátí kompletní přehled dne. Bez parametru vrací dnešek. */ +router.get("/", async (req: Request, res: Response, next: NextFunction) => { + try { + const login = getLogin(parseToken(req)); + const date = req.query.date == null + ? formatDate(getToday()) + : requireIsoDate(req.query.date, 'date'); + res.status(200).json(await getDayOverview(login, date)); + } catch (e) { next(e); } +}); + +export default router; diff --git a/server/src/routes/importRoutes.ts b/server/src/routes/importRoutes.ts new file mode 100644 index 0000000..99252fa --- /dev/null +++ b/server/src/routes/importRoutes.ts @@ -0,0 +1,33 @@ +import express, { Request, Response, NextFunction } from "express"; +import { importLuncherExport } from "../luncherImport"; +import { getLogin } from "../auth"; +import { BadRequestError, parseToken } from "../utils"; + +const router = express.Router(); + +/** + * Naimportuje měsíční přehled vyexportovaný ze stránky statistik Luncheru. + * + * Soubor přichází v těle požadavku jako Base64 — vyhneme se tím multipartu + * a XLSX (binární formát) projde stejnou cestou jako CSV a JSON. + */ +router.post("/luncher", async (req: Request, res: Response, next: NextFunction) => { + try { + const login = getLogin(parseToken(req)); + const { fileName, content, defaultMealType, dryRun } = req.body ?? {}; + if (typeof fileName !== 'string' || !fileName.trim().length) { + throw new BadRequestError("Nebyl předán název souboru"); + } + if (typeof content !== 'string' || !content.length) { + throw new BadRequestError("Nebyl předán obsah souboru"); + } + const buffer = Buffer.from(content, 'base64'); + if (!buffer.length) { + throw new BadRequestError("Obsah souboru je prázdný nebo není platný Base64"); + } + const result = await importLuncherExport(login, fileName.trim(), buffer, defaultMealType, dryRun === true); + res.status(200).json(result); + } catch (e) { next(e); } +}); + +export default router; diff --git a/server/src/routes/mealRoutes.ts b/server/src/routes/mealRoutes.ts new file mode 100644 index 0000000..047e5c1 --- /dev/null +++ b/server/src/routes/mealRoutes.ts @@ -0,0 +1,59 @@ +import express, { Request, Response, NextFunction } from "express"; +import { addMeal, deleteMeal, getDay, getRange, updateMeal } from "../meals"; +import { getLogin } from "../auth"; +import { formatDate, getToday, parseToken, requireIsoDate } from "../utils"; + +const router = express.Router(); + +/** + * Vrátí jídla přihlášeného uživatele za jeden den. Bez parametru vrací dnešek. + */ +router.get("/day", async (req: Request, res: Response, next: NextFunction) => { + try { + const login = getLogin(parseToken(req)); + const date = req.query.date == null + ? formatDate(getToday()) + : requireIsoDate(req.query.date, 'date'); + res.status(200).json(await getDay(login, date)); + } catch (e) { next(e); } +}); + +/** + * Vrátí dny přihlášeného uživatele v rozsahu, které obsahují alespoň jeden záznam. + */ +router.get("/range", async (req: Request, res: Response, next: NextFunction) => { + try { + const login = getLogin(parseToken(req)); + const from = requireIsoDate(req.query.from, 'from'); + const to = requireIsoDate(req.query.to, 'to'); + res.status(200).json(await getRange(login, from, to)); + } catch (e) { next(e); } +}); + +/** Přidá jídlo do zvoleného dne. */ +router.post("/add", async (req: Request, res: Response, next: NextFunction) => { + try { + const login = getLogin(parseToken(req)); + res.status(200).json(await addMeal(login, req.body)); + } catch (e) { next(e); } +}); + +/** Upraví existující záznam o jídle. */ +router.post("/update", async (req: Request, res: Response, next: NextFunction) => { + try { + const login = getLogin(parseToken(req)); + const { id, ...input } = req.body ?? {}; + res.status(200).json(await updateMeal(login, id, input)); + } catch (e) { next(e); } +}); + +/** Smaže záznam o jídle. */ +router.post("/delete", async (req: Request, res: Response, next: NextFunction) => { + try { + const login = getLogin(parseToken(req)); + const { id, date } = req.body ?? {}; + res.status(200).json(await deleteMeal(login, id, date)); + } catch (e) { next(e); } +}); + +export default router; diff --git a/server/src/routes/settingsRoutes.ts b/server/src/routes/settingsRoutes.ts new file mode 100644 index 0000000..e851fbc --- /dev/null +++ b/server/src/routes/settingsRoutes.ts @@ -0,0 +1,33 @@ +import express, { Request, Response, NextFunction } from "express"; +import { getSettings, saveBasalCalories, saveSourceRate } from "../settings"; +import { getLogin } from "../auth"; +import { parseToken } from "../utils"; + +const router = express.Router(); + +/** Vrátí nastavení přihlášeného uživatele. */ +router.get("/", async (req: Request, res: Response, next: NextFunction) => { + try { + const login = getLogin(parseToken(req)); + res.status(200).json(await getSettings(login)); + } catch (e) { next(e); } +}); + +/** Uloží nebo smaže cenu za 100 g u jednoho zdroje. */ +router.post("/", async (req: Request, res: Response, next: NextFunction) => { + try { + const login = getLogin(parseToken(req)); + const { source, pricePer100g } = req.body ?? {}; + res.status(200).json(await saveSourceRate(login, source, pricePer100g)); + } catch (e) { next(e); } +}); + +/** Uloží klidový výdej (bazální metabolismus). */ +router.post("/basal", async (req: Request, res: Response, next: NextFunction) => { + try { + const login = getLogin(parseToken(req)); + res.status(200).json(await saveBasalCalories(login, req.body?.basalCalories)); + } catch (e) { next(e); } +}); + +export default router; diff --git a/server/src/routes/statsRoutes.ts b/server/src/routes/statsRoutes.ts new file mode 100644 index 0000000..252a053 --- /dev/null +++ b/server/src/routes/statsRoutes.ts @@ -0,0 +1,18 @@ +import express, { Request, Response, NextFunction } from "express"; +import { getFoodStats } from "../statsService"; +import { getLogin } from "../auth"; +import { parseToken, requireIsoDate } from "../utils"; + +const router = express.Router(); + +/** Vrátí statistiky útraty a jídel přihlášeného uživatele za zvolené období. */ +router.get("/summary", async (req: Request, res: Response, next: NextFunction) => { + try { + const login = getLogin(parseToken(req)); + const from = requireIsoDate(req.query.from, 'from'); + const to = requireIsoDate(req.query.to, 'to'); + res.status(200).json(await getFoodStats(login, from, to)); + } catch (e) { next(e); } +}); + +export default router; diff --git a/server/src/routes/workoutRoutes.ts b/server/src/routes/workoutRoutes.ts new file mode 100644 index 0000000..731f2ea --- /dev/null +++ b/server/src/routes/workoutRoutes.ts @@ -0,0 +1,44 @@ +import express, { Request, Response, NextFunction } from "express"; +import { applyWorkoutTemplate, deleteWorkoutTemplate, getWorkoutTemplates, saveWorkoutTemplate } from "../workouts"; +import { getDayOverview } from "../dayOverview"; +import { getLogin } from "../auth"; +import { parseToken } from "../utils"; + +const router = express.Router(); + +/** Vrátí šablony tréninků přihlášeného uživatele. */ +router.get("/", async (req: Request, res: Response, next: NextFunction) => { + try { + const login = getLogin(parseToken(req)); + res.status(200).json(await getWorkoutTemplates(login)); + } catch (e) { next(e); } +}); + +/** Založí novou šablonu tréninku, nebo upraví existující. */ +router.post("/save", async (req: Request, res: Response, next: NextFunction) => { + try { + const login = getLogin(parseToken(req)); + const { id, name, items } = req.body ?? {}; + res.status(200).json(await saveWorkoutTemplate(login, id, name, items)); + } catch (e) { next(e); } +}); + +/** Smaže šablonu tréninku. */ +router.post("/delete", async (req: Request, res: Response, next: NextFunction) => { + try { + const login = getLogin(parseToken(req)); + res.status(200).json(await deleteWorkoutTemplate(login, req.body?.id)); + } catch (e) { next(e); } +}); + +/** Založí do zvoleného dne všechny položky šablony. */ +router.post("/apply", async (req: Request, res: Response, next: NextFunction) => { + try { + const login = getLogin(parseToken(req)); + const { id, date } = req.body ?? {}; + const day = await applyWorkoutTemplate(login, id, date); + res.status(200).json(await getDayOverview(login, day)); + } catch (e) { next(e); } +}); + +export default router; diff --git a/server/src/settings.ts b/server/src/settings.ts new file mode 100644 index 0000000..9205b9c --- /dev/null +++ b/server/src/settings.ts @@ -0,0 +1,101 @@ +import { UserSettings } from "../../types/gen/types.gen"; +import getStorage from "./storage"; +import { BadRequestError } from "./utils"; +import { normalizeName } from "./calories"; + +const storage = getStorage(); + +/** Horní mez sazby (10 000 Kč za 100 g) — chrání před překlepem o řád. */ +const MAX_PRICE_PER_100G = 1_000_000; + +/** Horní mez klidového výdeje — nad 10 000 kcal/den už jde o překlep. */ +const MAX_BASAL_CALORIES = 10_000; + +/** Klíč úložiště s nastavením jednoho uživatele. */ +function getSettingsKey(login: string): string { + return `settings:${login}`; +} + +/** Prázdné nastavení pro uživatele, který si zatím nic nenastavil. */ +function emptySettings(): UserSettings { + return { sourceRates: [] }; +} + +/** + * Vrátí nastavení uživatele. + * + * @param login přihlašovací jméno uživatele + */ +export async function getSettings(login: string): Promise { + return await storage.getData(getSettingsKey(login)) ?? emptySettings(); +} + +/** + * Uloží cenu za 100 g u jednoho zdroje. Prázdná hodnota sazbu odstraní. + * + * Zdroje se porovnávají znormalizovaně, aby "TechTower" a "techtower" + * neskončily jako dvě různé sazby. + * + * @param login přihlašovací jméno uživatele + * @param source název zdroje (podniku) + * @param pricePer100g cena za 100 g v haléřích, nebo null pro odstranění + */ +export async function saveSourceRate(login: string, source: unknown, pricePer100g: unknown): Promise { + if (typeof source !== 'string' || !source.trim().length) { + throw new BadRequestError("Nebyl předán název zdroje"); + } + if (pricePer100g != null && (typeof pricePer100g !== 'number' || !Number.isFinite(pricePer100g))) { + throw new BadRequestError("Cena za 100 g musí být číslo"); + } + const rate = pricePer100g == null ? 0 : Math.round(pricePer100g); + if (rate < 0 || rate > MAX_PRICE_PER_100G) { + throw new BadRequestError("Cena za 100 g je mimo rozsah"); + } + + const name = source.trim(); + const key = normalizeName(name); + return await storage.updateData(getSettingsKey(login), current => { + const rates = (current ?? emptySettings()).sourceRates.filter(item => normalizeName(item.source) !== key); + // Nulová sazba znamená smazání — ukládat ji nemá smysl + return { sourceRates: rate > 0 ? [...rates, { source: name, pricePer100g: rate }] : rates }; + }); +} + +/** + * Uloží klidový výdej (bazální metabolismus) v kcal za den. + * Prázdná nebo nulová hodnota ho odstraní a bilance dne pak nepočítá deficit. + * + * @param login přihlašovací jméno uživatele + * @param basalCalories klidový výdej v kcal, nebo null pro odstranění + */ +export async function saveBasalCalories(login: string, basalCalories: unknown): Promise { + if (basalCalories != null && (typeof basalCalories !== 'number' || !Number.isFinite(basalCalories))) { + throw new BadRequestError("Klidový výdej musí být číslo"); + } + const value = basalCalories == null ? 0 : Math.round(basalCalories); + if (value < 0 || value > MAX_BASAL_CALORIES) { + throw new BadRequestError("Klidový výdej je mimo rozsah"); + } + return await storage.updateData(getSettingsKey(login), current => { + const settings = current ?? emptySettings(); + // Nula znamená smazání — ukládat ji nemá smysl + if (value > 0) { + return { ...settings, basalCalories: value }; + } + const { basalCalories: _removed, ...rest } = settings; + return rest; + }); +} + +/** + * Vrátí sazbu za 100 g nastavenou pro daný zdroj, pokud existuje. + * + * @param login přihlašovací jméno uživatele + * @param source název zdroje + */ +export async function getSourceRate(login: string, source?: string): Promise { + if (!source?.trim().length) return undefined; + const key = normalizeName(source); + const settings = await getSettings(login); + return settings.sourceRates.find(rate => normalizeName(rate.source) === key)?.pricePer100g; +} diff --git a/server/src/statsService.ts b/server/src/statsService.ts new file mode 100644 index 0000000..1a78b56 --- /dev/null +++ b/server/src/statsService.ts @@ -0,0 +1,82 @@ +import { FoodStats, GroupTotal, MealEntry, PeriodTotal } from "../../types/gen/types.gen"; +import { getRange } from "./meals"; + +/** Zdroj přiřazený záznamům, které ho nemají vyplněný. */ +const UNKNOWN_SOURCE = 'Neuvedeno'; + +/** Průběžně načítaný součet jedné skupiny nebo období. */ +type Bucket = { price: number, calories: number, count: number }; + +/** Přičte záznam do kbelíku pod daným klíčem. */ +function accumulate(buckets: Map, key: string, entry: MealEntry): void { + const bucket = buckets.get(key) ?? { price: 0, calories: 0, count: 0 }; + bucket.price += entry.price ?? 0; + bucket.calories += entry.calories ?? 0; + bucket.count++; + buckets.set(key, bucket); +} + +/** Převede kbelíky na součty období seřazené vzestupně dle označení období. */ +function toPeriodTotals(buckets: Map): PeriodTotal[] { + return Array.from(buckets.entries()) + .map(([period, bucket]) => ({ period, ...bucket })) + .sort((a, b) => a.period.localeCompare(b.period)); +} + +/** Převede kbelíky na součty skupin seřazené sestupně dle útraty. */ +function toGroupTotals(buckets: Map): GroupTotal[] { + return Array.from(buckets.entries()) + .map(([key, bucket]) => ({ key, ...bucket })) + .sort((a, b) => b.price - a.price || b.count - a.count || a.key.localeCompare(b.key)); +} + +/** + * Sestaví statistiky útraty a jídel uživatele za zvolené období. + * + * Dny bez záznamu se do `dayCount` ani do `byDay` nepromítají — průměr na den + * tak vychází z reálně vykázaných dní, ne z délky vybraného rozsahu. + * + * @param login přihlašovací jméno uživatele + * @param from první den období (včetně), YYYY-MM-DD + * @param to poslední den období (včetně), YYYY-MM-DD + */ +export async function getFoodStats(login: string, from: string, to: string): Promise { + const days = await getRange(login, from, to); + + const byDay = new Map(); + const byMonth = new Map(); + const byYear = new Map(); + const byMealType = new Map(); + const bySource = new Map(); + + let totalPrice = 0; + let totalCalories = 0; + let entryCount = 0; + + for (const day of days) { + for (const entry of day.entries) { + accumulate(byDay, entry.date, entry); + accumulate(byMonth, entry.date.substring(0, 7), entry); + accumulate(byYear, entry.date.substring(0, 4), entry); + accumulate(byMealType, entry.mealType, entry); + accumulate(bySource, entry.source ?? UNKNOWN_SOURCE, entry); + totalPrice += entry.price ?? 0; + totalCalories += entry.calories ?? 0; + entryCount++; + } + } + + return { + from, + to, + totalPrice, + totalCalories, + entryCount, + dayCount: days.length, + byDay: toPeriodTotals(byDay), + byMonth: toPeriodTotals(byMonth), + byYear: toPeriodTotals(byYear), + byMealType: toGroupTotals(byMealType), + bySource: toGroupTotals(bySource), + }; +} diff --git a/server/src/storage/StorageInterface.ts b/server/src/storage/StorageInterface.ts new file mode 100644 index 0000000..722572a --- /dev/null +++ b/server/src/storage/StorageInterface.ts @@ -0,0 +1,29 @@ +/** + * Interface pro úložiště dat. + */ +export interface StorageInterface { + + initialize?(): Promise; + + hasData(key: string): Promise; + + getData(key: string): Promise; + + setData(key: string, data: Type): Promise; + + /** + * Vrátí seznam všech klíčů, případně jen těch obsahujících předaný podřetězec. + * @param contains volitelný podřetězec, který musí klíč obsahovat + */ + listKeys(contains?: string): Promise; + + /** + * Atomicky načte, zmutuje a uloží data pod daným klíčem. + * V Redis implementaci používá WATCH/MULTI/EXEC retry loop. + * Vrátí výslednou hodnotu po aplikaci mutátoru. + */ + updateData(key: string, mutator: (current: Type | undefined) => Type): Promise; + + /** Ověří dostupnost úložiště. Vrátí false pokud není dostupné. */ + healthCheck?(): Promise; +} diff --git a/server/src/storage/index.ts b/server/src/storage/index.ts new file mode 100644 index 0000000..fe6a468 --- /dev/null +++ b/server/src/storage/index.ts @@ -0,0 +1,32 @@ +import dotenv from 'dotenv'; +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 new Error("Nepodporovaná hodnota proměnné STORAGE: " + process.env.STORAGE + ", podporované jsou 'json', 'redis' nebo 'memory'"); +} + +export const storageReady: Promise = storage.initialize + ? storage.initialize() + : Promise.resolve(); + +export default function getStorage(): StorageInterface { + return storage; +} diff --git a/server/src/storage/json.ts b/server/src/storage/json.ts new file mode 100644 index 0000000..9f57b32 --- /dev/null +++ b/server/src/storage/json.ts @@ -0,0 +1,48 @@ +import JSONdb from 'simple-json-db'; +import { StorageInterface } from "./StorageInterface"; +import * as fs from 'fs'; +import * as path from 'path'; + +const dbPath = path.resolve(__dirname, '../../data/db.json'); +const dbDir = path.dirname(dbPath); + +if (!fs.existsSync(dbDir)) { + fs.mkdirSync(dbDir, { recursive: true }); +} + +const db = new JSONdb(dbPath); + +/** + * Implementace úložiště používající JSON soubor. Určená pro vývoj. + */ +export default class JsonStorage implements StorageInterface { + + hasData(key: string): Promise { + return Promise.resolve(db.has(key)); + } + + getData(key: string): Promise { + return Promise.resolve(db.get(key)); + } + + setData(key: string, data: Type): Promise { + db.set(key, data); + return Promise.resolve(); + } + + listKeys(contains?: string): Promise { + const keys = Object.keys(db.JSON()); + return Promise.resolve(contains ? keys.filter(k => k.includes(contains)) : keys); + } + + updateData(key: string, mutator: (current: Type | undefined) => Type): Promise { + const current = db.get(key) as Type | undefined; + const next = mutator(current); + db.set(key, next); + return Promise.resolve(next); + } + + healthCheck(): Promise { + return Promise.resolve(true); + } +} diff --git a/server/src/storage/memory.ts b/server/src/storage/memory.ts new file mode 100644 index 0000000..8862f05 --- /dev/null +++ b/server/src/storage/memory.ts @@ -0,0 +1,43 @@ +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(); + } + + listKeys(contains?: string): Promise { + const keys = Array.from(store.keys()); + return Promise.resolve(contains ? keys.filter(k => k.includes(contains)) : keys); + } + + updateData(key: string, mutator: (current: Type | undefined) => Type): Promise { + const current = store.get(key) as Type | undefined; + const next = mutator(current); + store.set(key, next); + return Promise.resolve(next); + } + + healthCheck(): Promise { + return Promise.resolve(true); + } +} diff --git a/server/src/storage/redis.ts b/server/src/storage/redis.ts new file mode 100644 index 0000000..5da62bc --- /dev/null +++ b/server/src/storage/redis.ts @@ -0,0 +1,92 @@ +import { createClient } from 'redis'; +import { StorageInterface } from "./StorageInterface"; + +/** Počet pokusů o atomický zápis, než se WATCH/MULTI/EXEC smyčka vzdá. */ +const MAX_RETRIES = 10; + +/** + * Typ klienta se odvozuje z {@link createClient}, ne z exportovaného `RedisClientType`. + * Ten je generický přes registrované moduly a bez parametrů z něj vypadnou metody + * jako `executeIsolated`. + */ +type Client = ReturnType; + +let client: Client | undefined; + +/** Ukončí spojení hlavního Redis klienta (volá se při graceful shutdownu). */ +export async function shutdownRedisStorage(): Promise { + if (client?.isOpen) { + await client.quit(); + } +} + +/** + * Implementace úložiště nad Redisem. Určená pro produkční provoz. + */ +export default class RedisStorage implements StorageInterface { + + async initialize(): Promise { + const host = process.env.REDIS_HOST ?? 'localhost'; + const port = Number(process.env.REDIS_PORT ?? 6379); + client = createClient({ socket: { host, port } }); + client.on('error', error => console.error('Chyba Redisu:', error)); + await client.connect(); + console.log(`Připojeno k Redisu na ${host}:${port}`); + } + + private getClient(): Client { + if (!client) { + throw new Error('Redis klient není inicializovaný'); + } + return client; + } + + async hasData(key: string): Promise { + return (await this.getClient().exists(key)) === 1; + } + + async getData(key: string): Promise { + const value = await this.getClient().get(key); + return value == null ? undefined : JSON.parse(value) as Type; + } + + async setData(key: string, data: Type): Promise { + await this.getClient().set(key, JSON.stringify(data)); + } + + async listKeys(contains?: string): Promise { + const keys: string[] = []; + for await (const key of this.getClient().scanIterator({ MATCH: contains ? `*${contains}*` : '*', COUNT: 1000 })) { + keys.push(...(Array.isArray(key) ? key : [key])); + } + return keys; + } + + async updateData(key: string, mutator: (current: Type | undefined) => Type): Promise { + for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { + // Izolovaný klient, aby WATCH nekolidoval s ostatními požadavky + const attemptWrite = async (isolated: Client): Promise => { + await isolated.watch(key); + const value = await isolated.get(key); + const current = value == null ? undefined : JSON.parse(value) as Type; + const next = mutator(current); + const execResult = await isolated.multi().set(key, JSON.stringify(next)).exec(); + // null z EXEC = klíč se mezitím změnil, zkusíme znovu + return execResult === null ? undefined : next; + }; + const result = await this.getClient().executeIsolated(attemptWrite as never) as Type | undefined; + if (result !== undefined) { + return result; + } + } + throw new Error(`Nepodařilo se atomicky uložit klíč ${key} ani po ${MAX_RETRIES} pokusech`); + } + + async healthCheck(): Promise { + try { + return (await this.getClient().ping()) === 'PONG'; + } catch { + return false; + } + } +} diff --git a/server/src/tests/activities.test.ts b/server/src/tests/activities.test.ts new file mode 100644 index 0000000..e68a74d --- /dev/null +++ b/server/src/tests/activities.test.ts @@ -0,0 +1,231 @@ +process.env.CALORIE_PROVIDER = 'none'; + +import { + addActivity, deleteActivity, deriveActivityCalories, getActivityDay, updateActivity, +} from "../activities"; +import { applyWorkoutTemplate, deleteWorkoutTemplate, getWorkoutTemplates, saveWorkoutTemplate } from "../workouts"; +import { buildEnergyBalance, getDayOverview } from "../dayOverview"; +import { addMeal } from "../meals"; +import { saveBasalCalories } from "../settings"; +import { resetMemoryStorage } from "../storage/memory"; +import { ActivityUnit, MealType } from "../../../types/gen/types.gen"; + +const LOGIN = 'tester'; +const DATE = '2025-03-03'; + +/** Přibližná sazba chůze: 4 kcal na 100 kroků, tedy 400 kcal za 10 000 kroků. */ +const STEP_RATE = 4; + +function activity(overrides: Record = {}) { + return { + date: DATE, + name: 'Chůze', + unit: ActivityUnit.KROKY, + quantity: 10000, + caloriesPer100Units: STEP_RATE, + ...overrides, + }; +} + +beforeEach(() => resetMemoryStorage()); + +describe('evidence pohybu', () => { + + it('dopočte spálené kalorie z počtu kroků', () => { + const result = deriveActivityCalories({ + date: DATE, name: 'Chůze', unit: ActivityUnit.KROKY, + quantity: 10000, caloriesPer100Units: STEP_RATE, + }); + expect(result.calories).toBe(400); + }); + + it('bez sazby použije ručně zadané kalorie', () => { + const result = deriveActivityCalories({ + date: DATE, name: 'Plavání', unit: ActivityUnit.MINUTY, + quantity: 30, calories: 250, + }); + expect(result.calories).toBe(250); + }); + + it('přidá aktivitu a spočítá součet dne', async () => { + await addActivity(LOGIN, activity()); + await addActivity(LOGIN, activity({ + name: 'Kliky', unit: ActivityUnit.OPAKOVANI, quantity: 60, caloriesPer100Units: 50, + })); + + const day = await getActivityDay(LOGIN, DATE); + expect(day.entries).toHaveLength(2); + expect(day.totalCalories).toBe(430); + }); + + it('upraví aktivitu a přepočítá kalorie', async () => { + await addActivity(LOGIN, activity()); + const [entry] = (await getActivityDay(LOGIN, DATE)).entries; + + await updateActivity(LOGIN, entry.id, activity({ quantity: 5000 })); + + const day = await getActivityDay(LOGIN, DATE); + expect(day.entries[0].quantity).toBe(5000); + expect(day.totalCalories).toBe(200); + }); + + it('při změně data přesune aktivitu do jiného dne', async () => { + await addActivity(LOGIN, activity()); + const [entry] = (await getActivityDay(LOGIN, DATE)).entries; + + await updateActivity(LOGIN, entry.id, activity({ date: '2025-03-10' })); + + expect((await getActivityDay(LOGIN, DATE)).entries).toHaveLength(0); + expect((await getActivityDay(LOGIN, '2025-03-10')).entries).toHaveLength(1); + }); + + it('smaže aktivitu', async () => { + await addActivity(LOGIN, activity()); + const [entry] = (await getActivityDay(LOGIN, DATE)).entries; + + await deleteActivity(LOGIN, entry.id, DATE); + + expect((await getActivityDay(LOGIN, DATE)).totalCalories).toBe(0); + }); + + it('odmítne neplatný vstup', async () => { + await expect(addActivity(LOGIN, activity({ name: ' ' }))).rejects.toThrow(/název aktivity/); + await expect(addActivity(LOGIN, activity({ unit: 'SKOKY' }))).rejects.toThrow(/jednotka/); + await expect(addActivity(LOGIN, activity({ quantity: undefined }))).rejects.toThrow(/množství/); + await expect(addActivity(LOGIN, activity({ quantity: -5 }))).rejects.toThrow(/záporné/); + }); + + it('pohyb je oddělený mezi uživateli', async () => { + await addActivity(LOGIN, activity()); + + expect((await getActivityDay('nekdo-jiny', DATE)).entries).toHaveLength(0); + }); +}); + +describe('šablony tréninků', () => { + + const items = [ + { name: 'Kliky', unit: ActivityUnit.OPAKOVANI, quantity: 60, caloriesPer100Units: 50 }, + { name: 'Běh', unit: ActivityUnit.MINUTY, quantity: 20, caloriesPer100Units: 1000 }, + ]; + + it('uloží šablonu a spočítá odhad kalorií', async () => { + const templates = await saveWorkoutTemplate(LOGIN, undefined, 'Workout day 1', items); + + expect(templates).toHaveLength(1); + expect(templates[0].name).toBe('Workout day 1'); + // 60 kliků × 0,5 kcal + 20 minut × 10 kcal + expect(templates[0].estimatedCalories).toBe(230); + }); + + it('úprava podle id šablonu přepíše, nezaloží druhou', async () => { + const [created] = await saveWorkoutTemplate(LOGIN, undefined, 'Workout day 1', items); + + const templates = await saveWorkoutTemplate(LOGIN, created.id, 'Workout day 1 (těžší)', items); + + expect(templates).toHaveLength(1); + expect(templates[0].name).toBe('Workout day 1 (těžší)'); + }); + + it('použití šablony založí do dne všechny její položky', async () => { + const [template] = await saveWorkoutTemplate(LOGIN, undefined, 'Workout day 1', items); + + await applyWorkoutTemplate(LOGIN, template.id, DATE); + + const day = await getActivityDay(LOGIN, DATE); + expect(day.entries.map(entry => entry.name)).toEqual(['Kliky', 'Běh']); + expect(day.totalCalories).toBe(230); + expect(day.entries[0].templateId).toBe(template.id); + }); + + it('položky ze šablony jsou samostatné — úprava neovlivní šablonu', async () => { + const [template] = await saveWorkoutTemplate(LOGIN, undefined, 'Workout day 1', items); + await applyWorkoutTemplate(LOGIN, template.id, DATE); + const [entry] = (await getActivityDay(LOGIN, DATE)).entries; + + await updateActivity(LOGIN, entry.id, activity({ + name: 'Kliky', unit: ActivityUnit.OPAKOVANI, quantity: 100, caloriesPer100Units: 50, + })); + + expect((await getWorkoutTemplates(LOGIN))[0].items[0].quantity).toBe(60); + }); + + it('smaže šablonu', async () => { + const [template] = await saveWorkoutTemplate(LOGIN, undefined, 'Workout day 1', items); + + expect(await deleteWorkoutTemplate(LOGIN, template.id)).toEqual([]); + }); + + it('odmítne šablonu bez názvu nebo bez položek', async () => { + await expect(saveWorkoutTemplate(LOGIN, undefined, '', items)).rejects.toThrow(/název šablony/); + await expect(saveWorkoutTemplate(LOGIN, undefined, 'Prázdná', [])).rejects.toThrow(/alespoň jednu položku/); + }); + + it('hlásí chybu u neexistující šablony', async () => { + await expect(applyWorkoutTemplate(LOGIN, 'neexistuje', DATE)).rejects.toThrow(/nebyla nalezena/); + await expect(deleteWorkoutTemplate(LOGIN, 'neexistuje')).rejects.toThrow(/nebyla nalezena/); + }); + + it('šablony jsou oddělené mezi uživateli', async () => { + await saveWorkoutTemplate(LOGIN, undefined, 'Workout day 1', items); + + expect(await getWorkoutTemplates('nekdo-jiny')).toEqual([]); + }); +}); + +describe('energetická bilance dne', () => { + + it('bez klidového výdeje porovná jen jídlo proti pohybu', () => { + const energy = buildEnergyBalance(2000, 400); + + expect(energy.totalBurn).toBe(400); + expect(energy.balance).toBe(1600); + expect(energy.hasBasal).toBe(false); + }); + + it('s klidovým výdejem spočítá skutečný deficit', () => { + const energy = buildEnergyBalance(1800, 400, 1600); + + expect(energy.totalBurn).toBe(2000); + expect(energy.balance).toBe(-200); + expect(energy.hasBasal).toBe(true); + }); + + it('přebytek vyjde kladně', () => { + const energy = buildEnergyBalance(2500, 200, 1600); + + expect(energy.balance).toBe(700); + }); + + it('přehled dne spojí jídlo, pohyb i bilanci', async () => { + await saveBasalCalories(LOGIN, 1600); + await addMeal(LOGIN, { + date: DATE, mealType: MealType.OBED, name: 'Gyros', + weight: 400, caloriesPer100g: 172, price: 17600, + }); + await addActivity(LOGIN, activity()); + + const overview = await getDayOverview(LOGIN, DATE); + + expect(overview.meals.totalCalories).toBe(688); + expect(overview.activities.totalCalories).toBe(400); + expect(overview.energy.intake).toBe(688); + expect(overview.energy.totalBurn).toBe(2000); + expect(overview.energy.balance).toBe(-1312); + expect(overview.energy.hasBasal).toBe(true); + }); + + it('prázdný den vrátí nuly', async () => { + const overview = await getDayOverview(LOGIN, DATE); + + expect(overview.energy.intake).toBe(0); + expect(overview.energy.balance).toBe(0); + expect(overview.energy.hasBasal).toBe(false); + }); + + it('klidový výdej jde uložit i odstranit', async () => { + expect((await saveBasalCalories(LOGIN, 1600)).basalCalories).toBe(1600); + expect((await saveBasalCalories(LOGIN, 0)).basalCalories).toBeUndefined(); + await expect(saveBasalCalories(LOGIN, 99999)).rejects.toThrow(/mimo rozsah/); + }); +}); diff --git a/server/src/tests/calories.test.ts b/server/src/tests/calories.test.ts new file mode 100644 index 0000000..3e3b626 --- /dev/null +++ b/server/src/tests/calories.test.ts @@ -0,0 +1,200 @@ +// Externí poskytovatel se v testech nevolá — ověřujeme vlastní logiku, ne cizí službu +process.env.CALORIE_PROVIDER = 'none'; + +import { addMeal, deriveAmounts, getDay, updateMeal } from "../meals"; +import { searchCalories } from "../calories"; +import { getSettings, getSourceRate, saveSourceRate } from "../settings"; +import { resetMemoryStorage } from "../storage/memory"; +import { MealType } from "../../../types/gen/types.gen"; + +const LOGIN = 'tester'; + +/** Sazba TechTower: 44 Kč za 100 g, v haléřích. */ +const TOWER_RATE = 4400; + +function meal(overrides: Record = {}) { + return { date: '2025-03-03', mealType: MealType.OBED, name: 'Gyros', ...overrides }; +} + +beforeEach(() => resetMemoryStorage()); + +describe('dopočet gramáže a kalorií', () => { + + it('dopočte gramáž z ceny a sazby za 100 g', () => { + // 176 Kč při 44 Kč/100 g = 400 g + const result = deriveAmounts({ + date: '2025-03-03', mealType: MealType.OBED, name: 'Gyros', + price: 17600, pricePer100g: TOWER_RATE, + }); + expect(result.weight).toBe(400); + }); + + it('ručně zadaná gramáž má přednost před dopočtem z ceny', () => { + const result = deriveAmounts({ + date: '2025-03-03', mealType: MealType.OBED, name: 'Gyros', + price: 17600, pricePer100g: TOWER_RATE, weight: 350, + }); + expect(result.weight).toBe(350); + }); + + it('dopočte kalorie z gramáže a energie na 100 g', () => { + const result = deriveAmounts({ + date: '2025-03-03', mealType: MealType.OBED, name: 'Gyros', + weight: 400, caloriesPer100g: 172, + }); + expect(result.calories).toBe(688); + }); + + it('spojí oba dopočty — z ceny na gramáž a z gramáže na kalorie', () => { + const result = deriveAmounts({ + date: '2025-03-03', mealType: MealType.OBED, name: 'Gyros', + price: 17600, pricePer100g: TOWER_RATE, caloriesPer100g: 172, + }); + expect(result.weight).toBe(400); + expect(result.calories).toBe(688); + }); + + it('bez sazby nechá gramáž i kalorie na ručně zadaných hodnotách', () => { + const result = deriveAmounts({ + date: '2025-03-03', mealType: MealType.OBED, name: 'Gyros', + price: 17600, calories: 500, + }); + expect(result.weight).toBeUndefined(); + expect(result.calories).toBe(500); + }); + + it('různé položky téhož dne mohou mít různou sazbu', async () => { + // Hlavní jídlo za 44 Kč/100 g a salát s vlastním cenováním + await addMeal(LOGIN, meal({ name: 'Gyros', price: 17600, pricePer100g: TOWER_RATE, caloriesPer100g: 172 })); + const day = await addMeal(LOGIN, meal({ name: 'Salát', price: 6000, pricePer100g: 3000, caloriesPer100g: 40 })); + + const gyros = day.entries.find(e => e.name === 'Gyros')!; + const salat = day.entries.find(e => e.name === 'Salát')!; + expect(gyros.weight).toBe(400); + expect(gyros.calories).toBe(688); + expect(salat.weight).toBe(200); + expect(salat.calories).toBe(80); + expect(day.totalCalories).toBe(768); + expect(day.totalPrice).toBe(23600); + }); + + it('úprava umí gramáž i sazbu přepsat a vymazat', async () => { + const created = await addMeal(LOGIN, meal({ price: 17600, pricePer100g: TOWER_RATE, caloriesPer100g: 172 })); + const id = created.entries[0].id; + + const day = await updateMeal(LOGIN, id, meal({ price: 17600, weight: 500, caloriesPer100g: 172 })); + + expect(day.entries[0].weight).toBe(500); + expect(day.entries[0].pricePer100g).toBeUndefined(); + expect(day.entries[0].calories).toBe(860); + }); +}); + +describe('knihovna energetických hodnot', () => { + + it('zapamatuje si energii na 100 g a příště ji nabídne', async () => { + await addMeal(LOGIN, meal({ name: 'Kuřecí gyros', caloriesPer100g: 172, weight: 300 })); + + const result = await searchCalories(LOGIN, 'kuřecí gyros'); + + expect(result.suggestions).toEqual([ + { name: 'Kuřecí gyros', caloriesPer100g: 172, origin: 'library' }, + ]); + }); + + it('najde i při odlišné diakritice a velikosti písmen', async () => { + await addMeal(LOGIN, meal({ name: 'Kuřecí gyros', caloriesPer100g: 172, weight: 300 })); + + const result = await searchCalories(LOGIN, 'KURECI GYROS'); + + expect(result.suggestions[0].caloriesPer100g).toBe(172); + }); + + it('nabídne i částečnou shodu názvu', async () => { + await addMeal(LOGIN, meal({ name: 'Kuřecí gyros s hranolkami', caloriesPer100g: 172, weight: 300 })); + + const result = await searchCalories(LOGIN, 'gyros'); + + expect(result.suggestions[0].name).toBe('Kuřecí gyros s hranolkami'); + }); + + it('jídlo bez energie na 100 g si do knihovny neuloží', async () => { + await addMeal(LOGIN, meal({ name: 'Něco', calories: 500 })); + + const result = await searchCalories(LOGIN, 'Něco'); + + expect(result.suggestions).toEqual([]); + }); + + it('novější zadání přepíše dřívější hodnotu', async () => { + await addMeal(LOGIN, meal({ name: 'Gyros', caloriesPer100g: 172, weight: 300 })); + await addMeal(LOGIN, meal({ name: 'Gyros', caloriesPer100g: 180, weight: 300 })); + + const result = await searchCalories(LOGIN, 'Gyros'); + + expect(result.suggestions[0].caloriesPer100g).toBe(180); + }); + + it('knihovna je oddělená mezi uživateli', async () => { + await addMeal(LOGIN, meal({ name: 'Gyros', caloriesPer100g: 172, weight: 300 })); + + const result = await searchCalories('nekdo-jiny', 'Gyros'); + + expect(result.suggestions).toEqual([]); + }); + + it('vrátí odkaz na tabulku potravin na KalorickéTabulky.cz', async () => { + const result = await searchCalories(LOGIN, 'kuřecí gyros'); + + // Předvyplnit hledání nejde — jejich vyhledávání je v JS a URL parametry ignoruje + expect(result.externalSearchUrl).toBe('https://www.kaloricketabulky.cz/tabulka-potravin'); + }); + + it('odmítne prázdný dotaz', async () => { + await expect(searchCalories(LOGIN, ' ')).rejects.toThrow(/hledaný název/); + }); +}); + +describe('sazby za 100 g u zdrojů', () => { + + it('uloží a vrátí sazbu podniku', async () => { + await saveSourceRate(LOGIN, 'TechTower', TOWER_RATE); + + expect(await getSourceRate(LOGIN, 'TechTower')).toBe(TOWER_RATE); + expect((await getSettings(LOGIN)).sourceRates).toEqual([ + { source: 'TechTower', pricePer100g: TOWER_RATE }, + ]); + }); + + it('najde sazbu bez ohledu na velikost písmen', async () => { + await saveSourceRate(LOGIN, 'TechTower', TOWER_RATE); + + expect(await getSourceRate(LOGIN, 'techtower')).toBe(TOWER_RATE); + }); + + it('opakované uložení sazbu přepíše, nezaloží druhou', async () => { + await saveSourceRate(LOGIN, 'TechTower', TOWER_RATE); + const settings = await saveSourceRate(LOGIN, 'TechTower', 4800); + + expect(settings.sourceRates).toHaveLength(1); + expect(settings.sourceRates[0].pricePer100g).toBe(4800); + }); + + it('nulová sazba ji odstraní', async () => { + await saveSourceRate(LOGIN, 'TechTower', TOWER_RATE); + const settings = await saveSourceRate(LOGIN, 'TechTower', 0); + + expect(settings.sourceRates).toEqual([]); + }); + + it('odmítne prázdný zdroj a nesmyslnou sazbu', async () => { + await expect(saveSourceRate(LOGIN, '', TOWER_RATE)).rejects.toThrow(/název zdroje/); + await expect(saveSourceRate(LOGIN, 'TechTower', -1)).rejects.toThrow(/mimo rozsah/); + }); + + it('sazby jsou oddělené mezi uživateli', async () => { + await saveSourceRate(LOGIN, 'TechTower', TOWER_RATE); + + expect(await getSourceRate('nekdo-jiny', 'TechTower')).toBeUndefined(); + }); +}); diff --git a/server/src/tests/luncherImport.test.ts b/server/src/tests/luncherImport.test.ts new file mode 100644 index 0000000..4b276b3 --- /dev/null +++ b/server/src/tests/luncherImport.test.ts @@ -0,0 +1,280 @@ +import ExcelJS from 'exceljs'; +import { importLuncherExport } from "../luncherImport"; +import { getDay } from "../meals"; +import { resetMemoryStorage } from "../storage/memory"; +import { MealType } from "../../../types/gen/types.gen"; + +const LOGIN = 'tester'; + +/** Hlavička přehledu tak, jak ji generuje Luncher (server/src/userExport.ts). */ +const HEADERS = ['Datum', 'Den', 'Typ', 'Vybrané jídlo', 'Poznámka', 'Objednávka', 'Objednával', 'Částka']; + +/** Řádky používané napříč testy — pokrývají volbu podniku, pizzu i objednávku. */ +const ROWS = [ + { date: '2025-03-03', dayOfWeek: 'pondělí', type: 'Sladovnická', food: 'Svíčková na smetaně', note: '', store: '', orderedBy: '', amount: 159 }, + { date: '2025-03-04', dayOfWeek: 'úterý', type: 'Pizza day', food: 'Margherita (32cm)', note: 'bez oregana', store: '', orderedBy: '', amount: 215.5 }, + { date: '2025-03-05', dayOfWeek: 'středa', type: 'Budu objednávat', food: 'Kuřecí burger', note: '', store: 'Bolt Food', orderedBy: 'novak', amount: 249 }, +]; + +/** Sestaví CSV přesně tak, jak ho Luncher exportuje — BOM, ';' a desetinná čárka. */ +function buildLuncherCsv(): Buffer { + const escape = (value: string) => `"${value.replace(/"/g, '""')}"`; + const lines = [HEADERS.map(escape).join(';')]; + for (const row of ROWS) { + const [y, m, d] = row.date.split('-'); + lines.push([ + escape(`${d}.${m}.${y}`), + escape(row.dayOfWeek), + escape(row.type), + escape(row.food), + escape(row.note), + escape(row.store), + escape(row.orderedBy), + escape(row.amount.toFixed(2).replace('.', ',')), + ].join(';')); + } + return Buffer.from(`${lines.join('\r\n')}\r\n`, 'utf-8'); +} + +/** Sestaví JSON přehled ve schématu UserExport. */ +function buildLuncherJson(): Buffer { + return Buffer.from(JSON.stringify({ + login: 'tester', + year: 2025, + month: 3, + rowCount: ROWS.length, + totalAmount: ROWS.reduce((sum, row) => sum + row.amount, 0), + rows: ROWS.map(row => ({ + date: row.date, + dayOfWeek: row.dayOfWeek, + type: row.type, + ...(row.food ? { food: row.food } : {}), + ...(row.note ? { note: row.note } : {}), + ...(row.store ? { store: row.store } : {}), + ...(row.orderedBy ? { orderedBy: row.orderedBy } : {}), + amount: row.amount, + })), + }), 'utf-8'); +} + +/** Sestaví XLSX se dvěma listy — detailem a souhrnem — jako skutečný export. */ +async function buildLuncherXlsx(): Promise { + const workbook = new ExcelJS.Workbook(); + const sheet = workbook.addWorksheet('Přehled'); + sheet.addRow(HEADERS); + for (const row of ROWS) { + const [y, m, d] = row.date.split('-').map(Number); + sheet.addRow([ + new Date(Date.UTC(y, m - 1, d)), + row.dayOfWeek, + row.type, + row.food, + row.note, + row.store, + row.orderedBy, + row.amount, + ]); + } + // Souhrnný list má vlastní sloupce a import ho musí ignorovat + const summary = workbook.addWorksheet('Souhrn'); + summary.addRow(['Typ', 'Počet záznamů', 'Částka celkem']); + summary.addRow(['Celkem', ROWS.length, ROWS.reduce((sum, row) => sum + row.amount, 0)]); + return Buffer.from(await workbook.xlsx.writeBuffer()); +} + +beforeEach(() => resetMemoryStorage()); + +describe('import přehledu z Luncheru', () => { + + it.each([ + ['CSV', 'luncher-tester-2025-03.csv', () => buildLuncherCsv()], + ['JSON', 'luncher-tester-2025-03.json', () => buildLuncherJson()], + ['XLSX', 'luncher-tester-2025-03.xlsx', () => buildLuncherXlsx()], + ])('%s naimportuje všechny řádky do jejich dnů', async (_label, fileName, build) => { + const result = await importLuncherExport(LOGIN, fileName, await build()); + + expect(result.warnings).toEqual([]); + expect(result.rowCount).toBe(3); + expect(result.imported).toBe(3); + expect(result.skipped).toBe(0); + expect(result.days).toEqual(['2025-03-03', '2025-03-04', '2025-03-05']); + // 159 + 215,50 + 249 Kč v haléřích + expect(result.totalPrice).toBe(62350); + }); + + it('založí importované řádky jako oběd, pokud se neuvede jinak', async () => { + await importLuncherExport(LOGIN, 'export.json', buildLuncherJson()); + + const day = await getDay(LOGIN, '2025-03-03'); + expect(day.entries).toHaveLength(1); + expect(day.entries[0].mealType).toBe(MealType.OBED); + }); + + it('respektuje zvolený typ jídla', async () => { + await importLuncherExport(LOGIN, 'export.json', buildLuncherJson(), MealType.VECERE); + + const day = await getDay(LOGIN, '2025-03-03'); + expect(day.entries[0].mealType).toBe(MealType.VECERE); + }); + + it('mapuje jídlo, zdroj i cenu na záznam', async () => { + await importLuncherExport(LOGIN, 'export.json', buildLuncherJson()); + + const [entry] = (await getDay(LOGIN, '2025-03-03')).entries; + expect(entry.name).toBe('Svíčková na smetaně'); + expect(entry.source).toBe('Sladovnická'); + expect(entry.price).toBe(15900); + expect(entry.importSource).toBe('luncher'); + }); + + it('u objednávky bere jako zdroj obchod a objednávajícího přidá do poznámky', async () => { + await importLuncherExport(LOGIN, 'export.json', buildLuncherJson()); + + const [entry] = (await getDay(LOGIN, '2025-03-05')).entries; + expect(entry.name).toBe('Kuřecí burger'); + expect(entry.source).toBe('Bolt Food'); + expect(entry.note).toBe('objednal(a): novak'); + }); + + it('u volby bez jídla i poznámky použije jako název podnik', async () => { + const json = Buffer.from(JSON.stringify({ + rows: [{ date: '2025-03-06', dayOfWeek: 'čtvrtek', type: 'TechTower', amount: 145 }], + }), 'utf-8'); + + await importLuncherExport(LOGIN, 'export.json', json); + + const [entry] = (await getDay(LOGIN, '2025-03-06')).entries; + expect(entry.name).toBe('TechTower'); + expect(entry.source).toBe('TechTower'); + }); + + it('u "Budu objednávat" bez jídla vezme jako název poznámku', async () => { + // Skutečný tvar řádku z exportu: jídlo prázdné, obsah stojí v poznámce + const json = Buffer.from(JSON.stringify({ + rows: [{ + date: '2025-03-06', dayOfWeek: 'čtvrtek', + type: 'Budu objednávat', note: 'Chefie - Těstovinový salát', + }], + }), 'utf-8'); + + await importLuncherExport(LOGIN, 'export.json', json); + + const [entry] = (await getDay(LOGIN, '2025-03-06')).entries; + expect(entry.name).toBe('Chefie - Těstovinový salát'); + // Poznámka se stala názvem, nemá se opakovat + expect(entry.note).toBeUndefined(); + // "Budu objednávat" není podnik, jako zdroj nedává smysl + expect(entry.source).toBeUndefined(); + }); + + it('u "Rozhoduji se" vezme jako název poznámku a nenastaví zdroj', async () => { + const json = Buffer.from(JSON.stringify({ + rows: [{ date: '2025-03-07', dayOfWeek: 'pátek', type: 'Rozhoduji se', note: 'asi pudu tower' }], + }), 'utf-8'); + + await importLuncherExport(LOGIN, 'export.json', json); + + const [entry] = (await getDay(LOGIN, '2025-03-07')).entries; + expect(entry.name).toBe('asi pudu tower'); + expect(entry.source).toBeUndefined(); + }); + + it('když je vyplněné jídlo i poznámka, poznámka zůstane poznámkou', async () => { + const json = Buffer.from(JSON.stringify({ + rows: [{ + date: '2025-03-08', dayOfWeek: 'sobota', type: 'Budu objednávat', + food: 'Chicken tikka massala', note: 'EVEREST', + store: 'Everest', orderedBy: 'Anděl Ondřej', amount: 215.54, + }], + }), 'utf-8'); + + await importLuncherExport(LOGIN, 'export.json', json); + + const [entry] = (await getDay(LOGIN, '2025-03-08')).entries; + expect(entry.name).toBe('Chicken tikka massala'); + expect(entry.note).toBe('EVEREST | objednal(a): Anděl Ondřej'); + expect(entry.source).toBe('Everest'); + expect(entry.price).toBe(21554); + }); + + it('řádek bez částky uloží bez ceny', async () => { + const json = Buffer.from(JSON.stringify({ + rows: [{ date: '2025-03-06', dayOfWeek: 'čtvrtek', type: 'SPŠE', food: 'Guláš' }], + }), 'utf-8'); + + await importLuncherExport(LOGIN, 'export.json', json); + + const [entry] = (await getDay(LOGIN, '2025-03-06')).entries; + expect(entry.price).toBeUndefined(); + expect((await getDay(LOGIN, '2025-03-06')).totalPrice).toBe(0); + }); + + it('opakovaný import stejného měsíce data nezduplikuje', async () => { + await importLuncherExport(LOGIN, 'export.json', buildLuncherJson()); + const second = await importLuncherExport(LOGIN, 'export.json', buildLuncherJson()); + + expect(second.imported).toBe(0); + expect(second.skipped).toBe(3); + expect((await getDay(LOGIN, '2025-03-03')).entries).toHaveLength(1); + }); + + it('naimportuje i dva shodné záznamy v jednom dni', async () => { + const row = { date: '2025-03-07', dayOfWeek: 'pátek', type: 'Objednávka', food: 'Kebab', amount: 120 }; + const json = Buffer.from(JSON.stringify({ rows: [row, row] }), 'utf-8'); + + const first = await importLuncherExport(LOGIN, 'export.json', json); + expect(first.imported).toBe(2); + + // Ani při opakování se nesmí jeden z nich považovat za duplicitu toho druhého + const second = await importLuncherExport(LOGIN, 'export.json', json); + expect(second.imported).toBe(0); + expect(second.skipped).toBe(2); + expect((await getDay(LOGIN, '2025-03-07')).entries).toHaveLength(2); + }); + + it('dryRun vrátí náhled, ale nic neuloží', async () => { + const result = await importLuncherExport(LOGIN, 'export.json', buildLuncherJson(), undefined, true); + + expect(result.dryRun).toBe(true); + expect(result.imported).toBe(3); + expect(result.entries).toHaveLength(3); + expect((await getDay(LOGIN, '2025-03-03')).entries).toHaveLength(0); + }); + + it('poradí si s poznámkou obsahující oddělovač a uvozovky', async () => { + const csv = Buffer.from( + `"Datum";"Typ";"Vybrané jídlo";"Poznámka";"Částka"\r\n` + + `"03.03.2025";"Sladovnická";"Svíčková";"bez knedlíku; navíc ""extra"" omáčka";"159,00"\r\n`, + 'utf-8'); + + await importLuncherExport(LOGIN, 'export.csv', csv); + + const [entry] = (await getDay(LOGIN, '2025-03-03')).entries; + expect(entry.note).toBe('bez knedlíku; navíc "extra" omáčka'); + }); + + it('upozorní na řádek s nečitelným datem a ostatní naimportuje', async () => { + const json = Buffer.from(JSON.stringify({ + rows: [ + { date: 'nesmysl', type: 'Sladovnická', food: 'Svíčková' }, + { date: '2025-03-03', type: 'Sladovnická', food: 'Guláš' }, + ], + }), 'utf-8'); + + const result = await importLuncherExport(LOGIN, 'export.json', json); + + expect(result.imported).toBe(1); + expect(result.warnings).toHaveLength(1); + }); + + it('odmítne nepodporovaný formát souboru', async () => { + await expect(importLuncherExport(LOGIN, 'export.pdf', Buffer.from('cokoliv'))) + .rejects.toThrow(/Nepodporovaný formát/); + }); + + it('odmítne soubor bez očekávaných sloupců', async () => { + const csv = Buffer.from('"Sloupec A";"Sloupec B"\r\n"1";"2"\r\n', 'utf-8'); + await expect(importLuncherExport(LOGIN, 'export.csv', csv)) + .rejects.toThrow(/nepodařilo najít očekávané sloupce/); + }); +}); diff --git a/server/src/tests/meals.test.ts b/server/src/tests/meals.test.ts new file mode 100644 index 0000000..aedc416 --- /dev/null +++ b/server/src/tests/meals.test.ts @@ -0,0 +1,176 @@ +import { addMeal, deleteMeal, getDay, getRange, updateMeal } from "../meals"; +import { getFoodStats } from "../statsService"; +import { resetMemoryStorage } from "../storage/memory"; +import { MealType } from "../../../types/gen/types.gen"; + +const LOGIN = 'tester'; + +/** Zkratka pro založení jídla s rozumnými výchozími hodnotami. */ +function meal(overrides: Record = {}) { + return { + date: '2025-03-03', + mealType: MealType.OBED, + name: 'Svíčková', + price: 15900, + ...overrides, + }; +} + +beforeEach(() => resetMemoryStorage()); + +describe('evidence jídel', () => { + + it('přidá jídlo a spočítá součty dne', async () => { + await addMeal(LOGIN, meal({ name: 'Rohlík', mealType: MealType.SNIDANE, price: 500, calories: 130 })); + const day = await addMeal(LOGIN, meal({ price: 15900, calories: 850 })); + + expect(day.entries).toHaveLength(2); + expect(day.totalPrice).toBe(16400); + expect(day.totalCalories).toBe(980); + }); + + it('řadí jídla dne dle chodu, ne dle pořadí zadání', async () => { + await addMeal(LOGIN, meal({ name: 'Večeře', mealType: MealType.VECERE })); + await addMeal(LOGIN, meal({ name: 'Snídaně', mealType: MealType.SNIDANE })); + const day = await addMeal(LOGIN, meal({ name: 'Oběd', mealType: MealType.OBED })); + + expect(day.entries.map(entry => entry.name)).toEqual(['Snídaně', 'Oběd', 'Večeře']); + }); + + it('do součtu ceny nezapočítává jídla bez ceny', async () => { + await addMeal(LOGIN, meal({ price: 15900 })); + const day = await addMeal(LOGIN, meal({ name: 'Jablko', price: undefined })); + + expect(day.entries).toHaveLength(2); + expect(day.totalPrice).toBe(15900); + }); + + it('upraví existující jídlo', async () => { + const created = await addMeal(LOGIN, meal()); + const id = created.entries[0].id; + + const day = await updateMeal(LOGIN, id, meal({ name: 'Guláš', price: 12000 })); + + expect(day.entries).toHaveLength(1); + expect(day.entries[0].name).toBe('Guláš'); + expect(day.entries[0].price).toBe(12000); + expect(day.entries[0].updatedAt).toBeDefined(); + }); + + it('umí u jídla vymazat cenu i poznámku', async () => { + const created = await addMeal(LOGIN, meal({ price: 15900, note: 'dobré' })); + + const day = await updateMeal(LOGIN, created.entries[0].id, meal({ price: undefined, note: undefined })); + + expect(day.entries[0].price).toBeUndefined(); + expect(day.entries[0].note).toBeUndefined(); + expect(day.totalPrice).toBe(0); + }); + + it('při změně data přesune jídlo do jiného dne', async () => { + const created = await addMeal(LOGIN, meal()); + + const day = await updateMeal(LOGIN, created.entries[0].id, meal({ date: '2025-03-10' })); + + expect(day.date).toBe('2025-03-10'); + expect(day.entries).toHaveLength(1); + expect((await getDay(LOGIN, '2025-03-03')).entries).toHaveLength(0); + }); + + it('smaže jídlo', async () => { + const created = await addMeal(LOGIN, meal()); + + const day = await deleteMeal(LOGIN, created.entries[0].id, '2025-03-03'); + + expect(day.entries).toHaveLength(0); + expect(day.totalPrice).toBe(0); + }); + + it('hlásí chybu u neexistujícího záznamu', async () => { + await expect(deleteMeal(LOGIN, 'neexistuje', '2025-03-03')).rejects.toThrow(/nebyl nalezen/); + await expect(updateMeal(LOGIN, 'neexistuje', meal())).rejects.toThrow(/nebyl nalezen/); + }); + + it('odmítne jídlo bez názvu nebo s neplatným datem', async () => { + await expect(addMeal(LOGIN, meal({ name: ' ' }))).rejects.toThrow(/název jídla/); + await expect(addMeal(LOGIN, meal({ date: '3.3.2025' }))).rejects.toThrow(/platné datum/); + await expect(addMeal(LOGIN, meal({ date: '2025-02-31' }))).rejects.toThrow(/platné datum/); + await expect(addMeal(LOGIN, meal({ mealType: 'BRUNCH' }))).rejects.toThrow(/typ jídla/); + await expect(addMeal(LOGIN, meal({ price: -5 }))).rejects.toThrow(/záporné/); + }); + + it('data jednoho uživatele nevidí druhý', async () => { + await addMeal(LOGIN, meal()); + + expect((await getDay('nekdo-jiny', '2025-03-03')).entries).toHaveLength(0); + }); + + it('vrátí jen dny s alespoň jedním záznamem', async () => { + await addMeal(LOGIN, meal({ date: '2025-03-03' })); + await addMeal(LOGIN, meal({ date: '2025-03-07' })); + + const days = await getRange(LOGIN, '2025-03-01', '2025-03-31'); + + expect(days.map(day => day.date)).toEqual(['2025-03-03', '2025-03-07']); + }); +}); + +describe('statistiky', () => { + + beforeEach(async () => { + await addMeal(LOGIN, meal({ date: '2025-03-03', name: 'Svíčková', source: 'Sladovnická', price: 15900, calories: 850 })); + await addMeal(LOGIN, meal({ date: '2025-03-03', name: 'Rohlík', mealType: MealType.SNIDANE, source: 'Doma', price: 500 })); + await addMeal(LOGIN, meal({ date: '2025-04-10', name: 'Guláš', source: 'Sladovnická', price: 14000, calories: 700 })); + await addMeal(LOGIN, meal({ date: '2024-12-24', name: 'Kapr', source: 'Doma', price: 30000 })); + }); + + it('spočítá celkovou útratu a kalorie za období', async () => { + const stats = await getFoodStats(LOGIN, '2025-01-01', '2025-12-31'); + + expect(stats.totalPrice).toBe(30400); + expect(stats.totalCalories).toBe(1550); + expect(stats.entryCount).toBe(3); + expect(stats.dayCount).toBe(2); + }); + + it('rozpadne útratu po dnech, měsících a letech', async () => { + const stats = await getFoodStats(LOGIN, '2024-01-01', '2025-12-31'); + + expect(stats.byDay.map(total => total.period)).toEqual(['2024-12-24', '2025-03-03', '2025-04-10']); + expect(stats.byMonth).toEqual([ + { period: '2024-12', price: 30000, calories: 0, count: 1 }, + { period: '2025-03', price: 16400, calories: 850, count: 2 }, + { period: '2025-04', price: 14000, calories: 700, count: 1 }, + ]); + expect(stats.byYear).toEqual([ + { period: '2024', price: 30000, calories: 0, count: 1 }, + { period: '2025', price: 30400, calories: 1550, count: 3 }, + ]); + }); + + it('rozpadne útratu dle typu jídla a zdroje, sestupně', async () => { + const stats = await getFoodStats(LOGIN, '2025-01-01', '2025-12-31'); + + expect(stats.byMealType).toEqual([ + { key: MealType.OBED, price: 29900, calories: 1550, count: 2 }, + { key: MealType.SNIDANE, price: 500, calories: 0, count: 1 }, + ]); + expect(stats.bySource[0]).toEqual({ key: 'Sladovnická', price: 29900, calories: 1550, count: 2 }); + }); + + it('období bez záznamů vrátí nuly', async () => { + const stats = await getFoodStats(LOGIN, '2023-01-01', '2023-12-31'); + + expect(stats.totalPrice).toBe(0); + expect(stats.entryCount).toBe(0); + expect(stats.byDay).toEqual([]); + }); + + it('jídla bez zdroje spadnou do skupiny Neuvedeno', async () => { + await addMeal(LOGIN, meal({ date: '2025-05-01', name: 'Něco', source: undefined, price: 100 })); + + const stats = await getFoodStats(LOGIN, '2025-05-01', '2025-05-31'); + + expect(stats.bySource).toEqual([{ key: 'Neuvedeno', price: 100, calories: 0, count: 1 }]); + }); +}); diff --git a/server/src/tests/setupEnv.ts b/server/src/tests/setupEnv.ts new file mode 100644 index 0000000..5b0842f --- /dev/null +++ b/server/src/tests/setupEnv.ts @@ -0,0 +1,4 @@ +// Testy běží vždy nad in-memory úložištěm a s pevným klíčem, ať nezávisí na .env +process.env.STORAGE = 'memory'; +process.env.JWT_SECRET = 'testovaci-klic-ktery-ma-aspon-32-znaku-delky'; +process.env.NODE_ENV = 'test'; diff --git a/server/src/utils.ts b/server/src/utils.ts new file mode 100644 index 0000000..5f68cc4 --- /dev/null +++ b/server/src/utils.ts @@ -0,0 +1,75 @@ +import { Request } from 'express'; + +/** Regulární výraz pro datum ve formátu YYYY-MM-DD. */ +const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; + +/** Chyba způsobená neplatným vstupem uživatele — mapuje se na HTTP 400. */ +export class BadRequestError extends Error { } + +/** Chyba hledaného, ale neexistujícího záznamu — mapuje se na HTTP 404. */ +export class NotFoundError extends Error { } + +/** Vrátí datum v ISO formátu (YYYY-MM-DD) v lokální časové zóně. */ +export function formatDate(date: Date): string { + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const year = String(date.getFullYear()); + return `${year}-${month}-${day}`; +} + +/** Vrátí dnešní datum s vynulovaným časem. */ +export function getToday(): Date { + const today = new Date(); + today.setHours(0, 0, 0, 0); + return today; +} + +/** + * Ověří, že řetězec je datum ve formátu YYYY-MM-DD a odpovídá reálnému dni. + * Zachytí i přetečení typu 2024-02-31, které by `new Date` tiše posunul na březen. + */ +export function isValidIsoDate(value: unknown): value is string { + if (typeof value !== 'string' || !ISO_DATE_PATTERN.test(value)) { + return false; + } + const [year, month, day] = value.split('-').map(Number); + const date = new Date(year, month - 1, day); + return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day; +} + +/** + * Vrátí datum z query parametru, nebo vyhodí {@link BadRequestError}. + * + * @param value hodnota query parametru + * @param name název parametru pro chybovou hlášku + */ +export function requireIsoDate(value: unknown, name: string): string { + if (!isValidIsoDate(value)) { + throw new BadRequestError(`Parametr '${name}' není platné datum ve formátu YYYY-MM-DD`); + } + return value; +} + +/** Vrátí JWT token z hlavičky Authorization, pokud tam je. */ +export function parseToken(req: Request): string | undefined { + return req.headers?.authorization?.split(' ')[1]; +} + +/** + * Vrátí všechna data v rozsahu (včetně obou krajů) jako ISO řetězce. + * + * @param from první den rozsahu (YYYY-MM-DD) + * @param to poslední den rozsahu (YYYY-MM-DD) + */ +export function listDatesInRange(from: string, to: string): string[] { + const dates: string[] = []; + const [fromYear, fromMonth, fromDay] = from.split('-').map(Number); + const [toYear, toMonth, toDay] = to.split('-').map(Number); + const cursor = new Date(fromYear, fromMonth - 1, fromDay); + const end = new Date(toYear, toMonth - 1, toDay); + while (cursor <= end) { + dates.push(formatDate(cursor)); + cursor.setDate(cursor.getDate() + 1); + } + return dates; +} diff --git a/server/src/workouts.ts b/server/src/workouts.ts new file mode 100644 index 0000000..f375955 --- /dev/null +++ b/server/src/workouts.ts @@ -0,0 +1,161 @@ +import { randomUUID } from 'crypto'; +import { ActivityEntry, WorkoutTemplate, WorkoutTemplateItem } from "../../types/gen/types.gen"; +import getStorage from "./storage"; +import { addActivities, isActivityUnit } from "./activities"; +import { BadRequestError, NotFoundError, isValidIsoDate } from "./utils"; + +const storage = getStorage(); + +/** Maximální počet položek jedné šablony — pojistka proti nesmyslnému vstupu. */ +const MAX_ITEMS = 50; + +/** Klíč úložiště se šablonami tréninků jednoho uživatele. */ +function getKey(login: string): string { + return `workouts:${login}`; +} + +/** Spočítá odhad spálených kcal za celou šablonu. */ +function estimateCalories(items: WorkoutTemplateItem[]): number { + return items.reduce((sum, item) => sum + Math.round(item.quantity / 100 * (item.caloriesPer100Units ?? 0)), 0); +} + +/** Doplní šabloně dopočítaný odhad kalorií. */ +function withEstimate(template: Omit): WorkoutTemplate { + return { ...template, estimatedCalories: estimateCalories(template.items) }; +} + +/** Uložená podoba šablony — odhad kalorií se dopočítává, neukládá se. */ +type StoredTemplate = Omit; + +/** + * Vrátí šablony tréninků uživatele seřazené podle názvu. + * + * @param login přihlašovací jméno uživatele + */ +export async function getWorkoutTemplates(login: string): Promise { + const templates = await storage.getData(getKey(login)) ?? []; + return templates + .map(withEstimate) + .sort((a, b) => a.name.localeCompare(b.name, 'cs')); +} + +/** Ověří a znormalizuje jednu položku šablony. */ +function normalizeItem(raw: unknown, index: number): WorkoutTemplateItem { + const value = (raw ?? {}) as Record; + const name = typeof value.name === 'string' ? value.name.trim() : ''; + if (!name.length) { + throw new BadRequestError(`Položka ${index + 1} nemá název`); + } + if (!isActivityUnit(value.unit)) { + throw new BadRequestError(`Položka '${name}' nemá platnou jednotku`); + } + if (typeof value.quantity !== 'number' || !Number.isFinite(value.quantity) || value.quantity < 0) { + throw new BadRequestError(`Položka '${name}' nemá platné množství`); + } + const rate = value.caloriesPer100Units; + return { + name, + unit: value.unit, + quantity: Math.round(value.quantity), + ...(typeof rate === 'number' && Number.isFinite(rate) && rate >= 0 + ? { caloriesPer100Units: Math.round(rate) } + : {}), + }; +} + +/** + * Založí novou šablonu, nebo přepíše existující podle předaného id. + * + * @param login přihlašovací jméno uživatele + * @param id identifikátor upravované šablony, nebo nic pro založení nové + * @param name název šablony + * @param items položky šablony + */ +export async function saveWorkoutTemplate( + login: string, + id: unknown, + name: unknown, + items: unknown, +): Promise { + if (typeof name !== 'string' || !name.trim().length) { + throw new BadRequestError("Nebyl předán název šablony"); + } + if (!Array.isArray(items) || !items.length) { + throw new BadRequestError("Šablona musí mít alespoň jednu položku"); + } + if (items.length > MAX_ITEMS) { + throw new BadRequestError(`Šablona může mít nejvýše ${MAX_ITEMS} položek`); + } + const normalized: StoredTemplate = { + id: typeof id === 'string' && id.length ? id : randomUUID(), + name: name.trim(), + items: items.map(normalizeItem), + }; + + await storage.updateData(getKey(login), current => { + const others = (current ?? []).filter(template => template.id !== normalized.id); + return [...others, normalized]; + }); + return getWorkoutTemplates(login); +} + +/** + * Smaže šablonu tréninku. + * + * @param login přihlašovací jméno uživatele + * @param id identifikátor šablony + */ +export async function deleteWorkoutTemplate(login: string, id: unknown): Promise { + if (typeof id !== 'string' || !id.length) { + throw new BadRequestError("Nebyl předán identifikátor šablony"); + } + let found = false; + await storage.updateData(getKey(login), current => { + found = (current ?? []).some(template => template.id === id); + return (current ?? []).filter(template => template.id !== id); + }); + if (!found) { + throw new NotFoundError("Šablona nebyla nalezena"); + } + return getWorkoutTemplates(login); +} + +/** + * Založí do zvoleného dne všechny položky šablony. + * + * Vzniknou z nich běžné aktivity, takže je pak jde jednotlivě upravit nebo smazat, + * aniž by to ovlivnilo šablonu. `templateId` u nich zůstane jen jako informace, + * odkud přišly. + * + * @param login přihlašovací jméno uživatele + * @param id identifikátor šablony + * @param date den, do kterého se položky založí + */ +export async function applyWorkoutTemplate(login: string, id: unknown, date: unknown): Promise { + if (typeof id !== 'string' || !id.length) { + throw new BadRequestError("Nebyl předán identifikátor šablony"); + } + if (!isValidIsoDate(date)) { + throw new BadRequestError("Nebylo předáno platné datum ve formátu YYYY-MM-DD"); + } + const template = (await getWorkoutTemplates(login)).find(item => item.id === id); + if (!template) { + throw new NotFoundError("Šablona nebyla nalezena"); + } + + const now = new Date().toISOString(); + const entries: ActivityEntry[] = template.items.map(item => ({ + id: randomUUID(), + date, + name: item.name, + unit: item.unit, + quantity: item.quantity, + ...(item.caloriesPer100Units != null ? { caloriesPer100Units: item.caloriesPer100Units } : {}), + calories: Math.round(item.quantity / 100 * (item.caloriesPer100Units ?? 0)), + templateId: template.id, + createdAt: now, + })); + + await addActivities(login, date, entries); + return date; +} diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..412f041 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,20 @@ +{ + "include": [ + "src/**/*", + "../types/**/*" + ], + "exclude": [ + "src/tests/**/*" + ], + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "node16", + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "../", + "strict": true + } +} diff --git a/server/yarn.lock b/server/yarn.lock new file mode 100644 index 0000000..9563fa0 --- /dev/null +++ b/server/yarn.lock @@ -0,0 +1,4816 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.27.1", "@babel/code-frame@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.7.tgz#f2fbbfea87c44a21590ec515b778b2c26d8866e7" + integrity sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw== + dependencies: + "@babel/helper-validator-identifier" "^7.29.7" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/compat-data@^7.28.6", "@babel/compat-data@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.7.tgz#6f0237f0f36d2e51c0570a636faed9d2d0efe629" + integrity sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg== + +"@babel/core@^7.23.9", "@babel/core@^7.27.4", "@babel/core@^7.28.5": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.7.tgz#80c10b17248082968b57a857b91640971f2070f7" + integrity sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA== + dependencies: + "@babel/code-frame" "^7.29.7" + "@babel/generator" "^7.29.7" + "@babel/helper-compilation-targets" "^7.29.7" + "@babel/helper-module-transforms" "^7.29.7" + "@babel/helpers" "^7.29.7" + "@babel/parser" "^7.29.7" + "@babel/template" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/types" "^7.29.7" + "@jridgewell/remapping" "^2.3.5" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.27.5", "@babel/generator@^7.29.7", "@babel/generator@^7.29.8": + version "7.29.8" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.8.tgz#4b0b887885422643339e09022148a4c4ebaa4979" + integrity sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg== + dependencies: + "@babel/parser" "^7.29.8" + "@babel/types" "^7.29.8" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-annotate-as-pure@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz#c70fe3c6ecbdc3fd2dd1b0f498428b88b82ce47f" + integrity sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw== + dependencies: + "@babel/types" "^7.29.7" + +"@babel/helper-compilation-targets@^7.28.6", "@babel/helper-compilation-targets@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz#7a1def704302401c47f64fa85589e974ae217042" + integrity sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g== + dependencies: + "@babel/compat-data" "^7.29.7" + "@babel/helper-validator-option" "^7.29.7" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz#6eddf286f2ec418f740c91d60a83347c55838ddd" + integrity sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.29.7" + "@babel/helper-member-expression-to-functions" "^7.29.7" + "@babel/helper-optimise-call-expression" "^7.29.7" + "@babel/helper-replace-supers" "^7.29.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.29.7" + "@babel/traverse" "^7.29.7" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz#5d4c3f928f315cf6c4184ea2fc3b5b38745b2430" + integrity sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.29.7" + regexpu-core "^6.3.1" + semver "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.6.8": + version "0.6.8" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz#cf1e4462b613f2b54c41e6ff758d5dfcaa2c85d1" + integrity sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA== + dependencies: + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + debug "^4.4.3" + lodash.debounce "^4.0.8" + resolve "^1.22.11" + +"@babel/helper-globals@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz#f04a96fbd8473241b1079243f5b3f03a3010ab7b" + integrity sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA== + +"@babel/helper-member-expression-to-functions@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz#8dbdb3ce0b5c487e1aec10e13c9a43a500814df8" + integrity sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg== + dependencies: + "@babel/traverse" "^7.29.7" + "@babel/types" "^7.29.7" + +"@babel/helper-module-imports@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz#ef25048a518e828d7393fac5882ddd73921d7396" + integrity sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g== + dependencies: + "@babel/traverse" "^7.29.7" + "@babel/types" "^7.29.7" + +"@babel/helper-module-transforms@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz#b062747a5997ba138637201328bbff77960574ae" + integrity sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg== + dependencies: + "@babel/helper-module-imports" "^7.29.7" + "@babel/helper-validator-identifier" "^7.29.7" + "@babel/traverse" "^7.29.7" + +"@babel/helper-optimise-call-expression@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz#77b0b5b94f1997fa9d6e3125f445227b1faf9d85" + integrity sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong== + dependencies: + "@babel/types" "^7.29.7" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.28.6", "@babel/helper-plugin-utils@^7.29.7", "@babel/helper-plugin-utils@^7.8.0": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz#c0a0766f1a13617d8a17407d7ab8f9d486225ea4" + integrity sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw== + +"@babel/helper-remap-async-to-generator@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz#34b1f68dd75b86d31df781a29c3ff2df88da82e6" + integrity sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og== + dependencies: + "@babel/helper-annotate-as-pure" "^7.29.7" + "@babel/helper-wrap-function" "^7.29.7" + "@babel/traverse" "^7.29.7" + +"@babel/helper-replace-supers@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz#bc3c3964329043c79112e513c1b198f16589ac21" + integrity sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.29.7" + "@babel/helper-optimise-call-expression" "^7.29.7" + "@babel/traverse" "^7.29.7" + +"@babel/helper-skip-transparent-expression-wrappers@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz#50c95c7e4c4f54936cfa0116428edc559862d551" + integrity sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ== + dependencies: + "@babel/traverse" "^7.29.7" + "@babel/types" "^7.29.7" + +"@babel/helper-string-parser@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz#7f0871d99824d23137d60f86fcf6130fd5a1b51f" + integrity sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw== + +"@babel/helper-validator-identifier@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz#bd87084ced0c796ec46bda492de6e83d29e89fc2" + integrity sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg== + +"@babel/helper-validator-option@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz#cf315be940213b354eb4abcc0bd01ebe3f73bc2a" + integrity sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw== + +"@babel/helper-wrap-function@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz#eec72163044548a0935e9d182bf2d547ec5ff483" + integrity sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw== + dependencies: + "@babel/template" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/types" "^7.29.7" + +"@babel/helpers@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.29.7.tgz#45abfde7548997e34376c3e69feb475cffb4a607" + integrity sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg== + dependencies: + "@babel/template" "^7.29.7" + "@babel/types" "^7.29.7" + +"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.29.7", "@babel/parser@^7.29.8": + version "7.29.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.8.tgz#9653716a2f10c677b98fbc63d4bfb000c302cf17" + integrity sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA== + dependencies: + "@babel/types" "^7.29.8" + +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz#2b535896d933a85aa92377eaa3d51a437d54a4e3" + integrity sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/traverse" "^7.29.7" + +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz#b00711a9e52bf4fe55ef7e54b2ef4a881bf804c8" + integrity sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz#2375328852026a3cf6bc0bcf2de7d236f2d5e701" + integrity sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz#759a857c46c4d2a6199685cf71070d81ae5f743a" + integrity sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.29.7" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz#86de98dd8e03836178231ea96c27dab26016a705" + integrity sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.29.7" + "@babel/plugin-transform-optional-chaining" "^7.29.7" + +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz#f5d892681dbf4b08753436a5e55000d5ba728d6d" + integrity sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/traverse" "^7.29.7" + +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + version "7.21.0-placeholder-for-preset-env.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-import-assertions@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz#c5cd868505269126cc18882e1f01f7b0e0e24b4e" + integrity sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-syntax-import-attributes@^7.24.7", "@babel/plugin-syntax-import-attributes@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz#6115264516e95ead0f35a41710906612e447f605" + integrity sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-syntax-import-meta@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.27.1", "@babel/plugin-syntax-jsx@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz#622c16f9ad63782fe6e83dadc7e40330744b7f1e" + integrity sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.27.1", "@babel/plugin-syntax-typescript@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz#7c29388932313ed58413a0343048d75d92fb5b24" + integrity sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-arrow-functions@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz#d651343f562c03f47951bd1802195d0e10605f27" + integrity sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-async-generator-functions@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz#a5365617921d82a1fee33124a1102bb38a1e677d" + integrity sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-remap-async-to-generator" "^7.29.7" + "@babel/traverse" "^7.29.7" + +"@babel/plugin-transform-async-to-generator@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz#3b5e8f1fb58133cf701bcf0baaf6f01bfd1a8889" + integrity sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w== + dependencies: + "@babel/helper-module-imports" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-remap-async-to-generator" "^7.29.7" + +"@babel/plugin-transform-block-scoped-functions@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz#96d292634434082d6687bcdb81139affedf77e8c" + integrity sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-block-scoping@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz#baa376691ae16244cd14335422fca6900f54e17d" + integrity sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-class-properties@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz#034897b8a21beec163332fac2de235b14409abdf" + integrity sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-class-static-block@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz#fed8efd19f3dd3e1114ee390707c70912778fd7c" + integrity sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-classes@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz#61d3e5aaae0c838acc3204d9db7c8dc05c25815b" + integrity sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g== + dependencies: + "@babel/helper-annotate-as-pure" "^7.29.7" + "@babel/helper-compilation-targets" "^7.29.7" + "@babel/helper-globals" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-replace-supers" "^7.29.7" + "@babel/traverse" "^7.29.7" + +"@babel/plugin-transform-computed-properties@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz#95028787ca31901b9a20b5c6d9605c32346f55ad" + integrity sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/template" "^7.29.7" + +"@babel/plugin-transform-destructuring@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz#5781ec6947852e27b64c1165f0db431f408090e4" + integrity sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/traverse" "^7.29.7" + +"@babel/plugin-transform-dotall-regex@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz#b203de9740e4c7ff6b55ce436ed5313b88d70af8" + integrity sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-duplicate-keys@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz#8f3fe721835cb7a433420841dae90afc962ea7ae" + integrity sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz#dc6c405e55c01b7657e1827a25332c4ac17e9cac" + integrity sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-dynamic-import@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz#a83a6faec5bab5b619adf9d0eac6c1c270123c2a" + integrity sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-explicit-resource-management@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz#65c8b9f76ec915b02a0e1df703125a0fca58abaa" + integrity sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-destructuring" "^7.29.7" + +"@babel/plugin-transform-exponentiation-operator@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz#00bf002fde8794356171f5d4df200f6bc0d5a303" + integrity sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-export-namespace-from@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz#d6014f45cec61d7691335c6c9804204bee801d51" + integrity sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-for-of@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz#c65a678592117717aacdb10c1b73a9cb85e830be" + integrity sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.29.7" + +"@babel/plugin-transform-function-name@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz#8b87f8a7504dbcd96135167e3fc4f61126a7bd86" + integrity sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg== + dependencies: + "@babel/helper-compilation-targets" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/traverse" "^7.29.7" + +"@babel/plugin-transform-json-strings@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz#f57d63dcc05b4481c281acedcd8fc4e3e439a1d4" + integrity sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-literals@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz#b90bd47463326c2a9d779e1bd5e1f88b9f421921" + integrity sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-logical-assignment-operators@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz#9b29425adf5c794967aabe4b046a046a167bac2f" + integrity sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-member-expression-literals@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz#1281689fa2fefc17b110d21ebafd0fe9402d5309" + integrity sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-modules-amd@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz#f05ca662c8a1dc4be2f337af9c7e80369c942d6c" + integrity sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew== + dependencies: + "@babel/helper-module-transforms" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-modules-commonjs@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz#70e6835abf2663dafbe94b8ef1f51de7351ef135" + integrity sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ== + dependencies: + "@babel/helper-module-transforms" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-modules-systemjs@^7.29.7": + version "7.29.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.8.tgz#e60a6a42ac63a3095f9cc7264f698a100c8fe05d" + integrity sha512-6iSnEK0zlkLKU4heofK/AdmRD4e2SHVpJMtrwnTCzhnaM98ria4rTrOXBBi45BTTYnJtO8txnPsX4fChYXkmeA== + dependencies: + "@babel/helper-module-transforms" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-validator-identifier" "^7.29.7" + "@babel/traverse" "^7.29.8" + +"@babel/plugin-transform-modules-umd@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz#391d1c0215aca6307257f2f608598dfe55feb6cf" + integrity sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA== + dependencies: + "@babel/helper-module-transforms" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz#21e75d847b31189842fa7a77703722ed4b43d27d" + integrity sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-new-target@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz#714147ce7947e1b49cbd84137ca2e75e92b2a067" + integrity sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz#8a54cdf88c3f50433a6173117a286195b67714cc" + integrity sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-numeric-separator@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz#0266d5cd42ab87ec40fee45a4e36483cfdcbc66a" + integrity sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-object-rest-spread@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz#e0d5060241803922c545676613cc8acbbda0d266" + integrity sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A== + dependencies: + "@babel/helper-compilation-targets" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-destructuring" "^7.29.7" + "@babel/plugin-transform-parameters" "^7.29.7" + "@babel/traverse" "^7.29.7" + +"@babel/plugin-transform-object-super@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz#e89283d14fa3c35817d4493ffc6bc649aa10e4eb" + integrity sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-replace-supers" "^7.29.7" + +"@babel/plugin-transform-optional-catch-binding@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz#729664f79985be504eba112c51de9f71d009030b" + integrity sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-optional-chaining@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz#b84a1b574b3c73001023092567e16c492b720e51" + integrity sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.29.7" + +"@babel/plugin-transform-parameters@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz#a5ddc3b9bfb534814cb8334cbeba47d9cf9db090" + integrity sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-private-methods@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz#cea8bd3ab99533892897a02999d5b752584ad145" + integrity sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-private-property-in-object@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz#4a2f6be5aba47be7afbdb4cd7903c46edf3a7661" + integrity sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.29.7" + "@babel/helper-create-class-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-property-literals@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz#d45817cd72f9e134ab1f7fbb79264cfcb85cf636" + integrity sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-regenerator@^7.29.7": + version "7.29.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.8.tgz#3a4a4dd7214af9d524f0503bb97c99af5f4abf26" + integrity sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-regexp-modifiers@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz#68311c0c10af2198212528863f8542843e424025" + integrity sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-reserved-words@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz#a6feeb179b36a5f1fc6e3154c1eb727bdbe35876" + integrity sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-shorthand-properties@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz#25c0436b98f4bd9ca4b98e1fbd662743bbaab9bf" + integrity sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-spread@^7.29.7": + version "7.29.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.8.tgz#c894cff38556a5dafeb412e13fc012b6df4b95a2" + integrity sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.29.7" + +"@babel/plugin-transform-sticky-regex@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz#a42c0fd1fa42f7e98e1e0c7757f72a1bbca3a015" + integrity sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-template-literals@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz#ada97d8e0832bca8edb315888aa654b1570f3835" + integrity sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-typeof-symbol@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz#d848a4677c1ee3485ab017f4018f04597798911c" + integrity sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-typescript@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz#f0449c3df7037bbe232043476851c38f5e4a7615" + integrity sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.29.7" + "@babel/helper-create-class-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.29.7" + "@babel/plugin-syntax-typescript" "^7.29.7" + +"@babel/plugin-transform-unicode-escapes@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz#1e99554b0cddfd650d649a9f2b996049893e5720" + integrity sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-unicode-property-regex@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz#44444afc73768c2190fac4d95f7716817b7f204a" + integrity sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-unicode-regex@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz#c3064b293ff7f1794b71f7650eec8db9896d3e59" + integrity sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-unicode-sets-regex@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz#b03ac9f27326f6197e8e574add83bbf33fc34ecd" + integrity sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/preset-env@^7.28.5": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.29.7.tgz#5e2ab5e764b493fdefc99c43aeaa70a9533a37fd" + integrity sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA== + dependencies: + "@babel/compat-data" "^7.29.7" + "@babel/helper-compilation-targets" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-validator-option" "^7.29.7" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.29.7" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.29.7" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.29.7" + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array" "^7.29.7" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.29.7" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.29.7" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-import-assertions" "^7.29.7" + "@babel/plugin-syntax-import-attributes" "^7.29.7" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.29.7" + "@babel/plugin-transform-async-generator-functions" "^7.29.7" + "@babel/plugin-transform-async-to-generator" "^7.29.7" + "@babel/plugin-transform-block-scoped-functions" "^7.29.7" + "@babel/plugin-transform-block-scoping" "^7.29.7" + "@babel/plugin-transform-class-properties" "^7.29.7" + "@babel/plugin-transform-class-static-block" "^7.29.7" + "@babel/plugin-transform-classes" "^7.29.7" + "@babel/plugin-transform-computed-properties" "^7.29.7" + "@babel/plugin-transform-destructuring" "^7.29.7" + "@babel/plugin-transform-dotall-regex" "^7.29.7" + "@babel/plugin-transform-duplicate-keys" "^7.29.7" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.29.7" + "@babel/plugin-transform-dynamic-import" "^7.29.7" + "@babel/plugin-transform-explicit-resource-management" "^7.29.7" + "@babel/plugin-transform-exponentiation-operator" "^7.29.7" + "@babel/plugin-transform-export-namespace-from" "^7.29.7" + "@babel/plugin-transform-for-of" "^7.29.7" + "@babel/plugin-transform-function-name" "^7.29.7" + "@babel/plugin-transform-json-strings" "^7.29.7" + "@babel/plugin-transform-literals" "^7.29.7" + "@babel/plugin-transform-logical-assignment-operators" "^7.29.7" + "@babel/plugin-transform-member-expression-literals" "^7.29.7" + "@babel/plugin-transform-modules-amd" "^7.29.7" + "@babel/plugin-transform-modules-commonjs" "^7.29.7" + "@babel/plugin-transform-modules-systemjs" "^7.29.7" + "@babel/plugin-transform-modules-umd" "^7.29.7" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.29.7" + "@babel/plugin-transform-new-target" "^7.29.7" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.29.7" + "@babel/plugin-transform-numeric-separator" "^7.29.7" + "@babel/plugin-transform-object-rest-spread" "^7.29.7" + "@babel/plugin-transform-object-super" "^7.29.7" + "@babel/plugin-transform-optional-catch-binding" "^7.29.7" + "@babel/plugin-transform-optional-chaining" "^7.29.7" + "@babel/plugin-transform-parameters" "^7.29.7" + "@babel/plugin-transform-private-methods" "^7.29.7" + "@babel/plugin-transform-private-property-in-object" "^7.29.7" + "@babel/plugin-transform-property-literals" "^7.29.7" + "@babel/plugin-transform-regenerator" "^7.29.7" + "@babel/plugin-transform-regexp-modifiers" "^7.29.7" + "@babel/plugin-transform-reserved-words" "^7.29.7" + "@babel/plugin-transform-shorthand-properties" "^7.29.7" + "@babel/plugin-transform-spread" "^7.29.7" + "@babel/plugin-transform-sticky-regex" "^7.29.7" + "@babel/plugin-transform-template-literals" "^7.29.7" + "@babel/plugin-transform-typeof-symbol" "^7.29.7" + "@babel/plugin-transform-unicode-escapes" "^7.29.7" + "@babel/plugin-transform-unicode-property-regex" "^7.29.7" + "@babel/plugin-transform-unicode-regex" "^7.29.7" + "@babel/plugin-transform-unicode-sets-regex" "^7.29.7" + "@babel/preset-modules" "0.1.6-no-external-plugins" + babel-plugin-polyfill-corejs2 "^0.4.15" + babel-plugin-polyfill-corejs3 "^0.14.0" + babel-plugin-polyfill-regenerator "^0.6.6" + core-js-compat "^3.48.0" + semver "^6.3.1" + +"@babel/preset-modules@0.1.6-no-external-plugins": + version "0.1.6-no-external-plugins" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" + integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/preset-typescript@^7.28.5": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz#de9be1f47b785c979ec7b3a71f4cd8bae5267b62" + integrity sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-validator-option" "^7.29.7" + "@babel/plugin-syntax-jsx" "^7.29.7" + "@babel/plugin-transform-modules-commonjs" "^7.29.7" + "@babel/plugin-transform-typescript" "^7.29.7" + +"@babel/template@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.29.7.tgz#4d9d4004f645cdd304de958c725162784ecac700" + integrity sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg== + dependencies: + "@babel/code-frame" "^7.29.7" + "@babel/parser" "^7.29.7" + "@babel/types" "^7.29.7" + +"@babel/traverse@^7.29.7", "@babel/traverse@^7.29.8": + version "7.29.8" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.8.tgz#4111014cdc71a0f95d9471907590baa0b8a6b28a" + integrity sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg== + dependencies: + "@babel/code-frame" "^7.29.7" + "@babel/generator" "^7.29.8" + "@babel/helper-globals" "^7.29.7" + "@babel/parser" "^7.29.8" + "@babel/template" "^7.29.7" + "@babel/types" "^7.29.8" + debug "^4.3.1" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.29.7", "@babel/types@^7.29.8", "@babel/types@^7.4.4": + version "7.29.8" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.8.tgz#1229eef31d85156d70fa3f4cd859376d0eaf6863" + integrity sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg== + dependencies: + "@babel/helper-string-parser" "^7.29.7" + "@babel/helper-validator-identifier" "^7.29.7" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@emnapi/core@1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.10.0.tgz#380ccc8f2412ea22d1d972df7f8ee23a3b9c7467" + integrity sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw== + dependencies: + "@emnapi/wasi-threads" "1.2.1" + tslib "^2.4.0" + +"@emnapi/runtime@1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.10.0.tgz#4b260c0d3534204e98c6110b8db1a987d26ec87c" + integrity sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA== + dependencies: + tslib "^2.4.0" + +"@emnapi/wasi-threads@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz#28fed21a1ba1ce797c44a070abc94d42f3ae8548" + integrity sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w== + dependencies: + tslib "^2.4.0" + +"@fast-csv/format@4.3.5": + version "4.3.5" + resolved "https://registry.yarnpkg.com/@fast-csv/format/-/format-4.3.5.tgz#90d83d1b47b6aaf67be70d6118f84f3e12ee1ff3" + integrity sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A== + dependencies: + "@types/node" "^14.0.1" + lodash.escaperegexp "^4.1.2" + lodash.isboolean "^3.0.3" + lodash.isequal "^4.5.0" + lodash.isfunction "^3.0.9" + lodash.isnil "^4.0.0" + +"@fast-csv/parse@4.3.6": + version "4.3.6" + resolved "https://registry.yarnpkg.com/@fast-csv/parse/-/parse-4.3.6.tgz#ee47d0640ca0291034c7aa94039a744cfb019264" + integrity sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA== + dependencies: + "@types/node" "^14.0.1" + lodash.escaperegexp "^4.1.2" + lodash.groupby "^4.6.0" + lodash.isfunction "^3.0.9" + lodash.isnil "^4.0.0" + lodash.isundefined "^3.0.1" + lodash.uniq "^4.5.0" + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": + version "0.1.6" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.6.tgz#8dc9afa2ac1506cb1a58f89940f1c124446c8df3" + integrity sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw== + +"@jest/console@30.5.1": + version "30.5.1" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-30.5.1.tgz#54a25bf4fee09a4e4c52b5a903ca7473d89297f6" + integrity sha512-u5Ncuc+gXVUwjNMFQOnphHo2Qx2DxyC8Dvpmf4HCx4y0kvSkRRNiQYRixrvOP4V7y52JcSuNxqochCt0PpdHVg== + dependencies: + "@jest/types" "30.5.1" + "@types/node" "*" + chalk "^4.1.2" + jest-message-util "30.5.1" + jest-util "30.5.1" + slash "^3.0.0" + +"@jest/core@30.5.1": + version "30.5.1" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-30.5.1.tgz#4e2eccc3367105d7b4bacc2fb4ccdbfa34a590f1" + integrity sha512-BL9g6CJUUhIbdoAflz/Va658erSSXUIvU8XUYiWNTbZljJjZ5yaC9EJ9/dQ19rpbYV3ZOK4sBACqhPaTyhoUIA== + dependencies: + "@jest/console" "30.5.1" + "@jest/pattern" "30.5.0" + "@jest/reporters" "30.5.1" + "@jest/test-result" "30.5.1" + "@jest/transform" "30.5.1" + "@jest/types" "30.5.1" + "@types/node" "*" + ansi-escapes "^4.3.2" + chalk "^4.1.2" + ci-info "^4.2.0" + exit-x "^0.2.2" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.11" + jest-changed-files "30.5.1" + jest-config "30.5.1" + jest-haste-map "30.5.1" + jest-message-util "30.5.1" + jest-regex-util "30.5.0" + jest-resolve "30.5.1" + jest-resolve-dependencies "30.5.1" + jest-runner "30.5.1" + jest-runtime "30.5.1" + jest-snapshot "30.5.1" + jest-util "30.5.1" + jest-validate "30.5.1" + jest-watcher "30.5.1" + pretty-format "30.5.1" + slash "^3.0.0" + +"@jest/diff-sequences@30.5.0": + version "30.5.0" + resolved "https://registry.yarnpkg.com/@jest/diff-sequences/-/diff-sequences-30.5.0.tgz#b896d470df751cc0c7d1a0c5078f80a67ce108d4" + integrity sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg== + +"@jest/environment@30.5.1": + version "30.5.1" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-30.5.1.tgz#a28a93864515da3ad90d3a841dc32a95be462c52" + integrity sha512-eYJAkOsrpwDXPcoLNEG6lN9Zo3Cy35pxnVQD74vyIfsi/Q8wB/lZFsEjU4wrecOgT4ZviQDZaX/cFIJyMdMxTw== + dependencies: + "@jest/fake-timers" "30.5.1" + "@jest/types" "30.5.1" + "@types/node" "*" + jest-mock "30.5.1" + +"@jest/expect-utils@30.5.1": + version "30.5.1" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-30.5.1.tgz#ae10e1698eff0800de971168aa783ead6a9403df" + integrity sha512-WcRWhHQdTMRDpyWKZ/6MINBmovI7zeD+bL8wFjCncRV3NQOwKy1X45IfyblfHR4k/XciIlNEdFL9QjFO+HNKOg== + dependencies: + "@jest/get-type" "30.5.0" + +"@jest/expect@30.5.1": + version "30.5.1" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-30.5.1.tgz#b42ce55ba35cc0cb2ee8aa823baec6b6fbb95d1f" + integrity sha512-uOGd40P/COyUp9xHf5jeGiGJC2/ANg+2+Tk9/xN5/LxmlY/r/gxsPrx3DTEtaRZsLAX4wgNSLEDELZ5bmVs2bA== + dependencies: + expect "30.5.1" + jest-snapshot "30.5.1" + +"@jest/fake-timers@30.5.1": + version "30.5.1" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-30.5.1.tgz#6e25f439f113216590a56e6423e716a2259a1255" + integrity sha512-rEkV6YzpBXo/L9cnj2ibyuYZuyGiNWaPUsyCu3HYJbqCV4jnEiKmf7hId20z8eKjZ0JQ9jtIYKxRSmYB/OYSsA== + dependencies: + "@jest/types" "30.5.1" + "@sinonjs/fake-timers" "^15.4.0" + "@types/node" "*" + jest-message-util "30.5.1" + jest-mock "30.5.1" + jest-util "30.5.1" + +"@jest/get-type@30.5.0": + version "30.5.0" + resolved "https://registry.yarnpkg.com/@jest/get-type/-/get-type-30.5.0.tgz#0fc76dd792523bf05d7715a18041c185f9128cc4" + integrity sha512-9/2VUPitAjmBzbvDvqrxmvB7BzWsBW0WmkkojX1ODuxX1NLGxx9gfaZpHB0z8DtJ9uhGNmZG/VXBhf8uO0OV8Q== + +"@jest/globals@30.5.1": + version "30.5.1" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-30.5.1.tgz#2794ea50e7ef7dea2675d04467b9eb7c42f17713" + integrity sha512-VhqvQ251XIC7pk46YymI3HCeBLOdvriDw9zibekodAHEwoVvbVVgpU5H13GvmyOAzxtZCfsm1uvzqkaqJf2I+g== + dependencies: + "@jest/environment" "30.5.1" + "@jest/expect" "30.5.1" + "@jest/types" "30.5.1" + jest-mock "30.5.1" + +"@jest/pattern@30.5.0": + version "30.5.0" + resolved "https://registry.yarnpkg.com/@jest/pattern/-/pattern-30.5.0.tgz#9f5dd0596a684b81eba2d7c1dfdca8d09978d326" + integrity sha512-HdNQYSdRTEBNrginaqzQtTjG0HRMfrra/z6Ok7uL3S87vSlarIVohEsJsSj5edu3MiHoHjAkvPROz5ZjoKai+w== + dependencies: + "@types/node" "*" + jest-regex-util "30.5.0" + +"@jest/react-is-18@npm:react-is@^18.3.1": + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + +"@jest/react-is-19@npm:react-is@^19.2.5": + version "19.2.8" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.2.8.tgz#09826f9fbc187bc668e3e5c62edc001f804d5018" + integrity sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ== + +"@jest/reporters@30.5.1": + version "30.5.1" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-30.5.1.tgz#5e7dd01fa614fee81ff140bf48d32057bfec23b7" + integrity sha512-RbUXIfv85KxitJn4l3MpAoMilkvXe2QCO5lXxHWftywo/VdZ5vkEoHRDgphcxP6qmoIkUaN8/UZj6NhdkIFdJg== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "30.5.1" + "@jest/test-result" "30.5.1" + "@jest/transform" "30.5.1" + "@jest/types" "30.5.1" + "@jridgewell/trace-mapping" "^0.3.31" + "@types/node" "*" + chalk "^4.1.2" + collect-v8-coverage "^1.0.2" + exit-x "^0.2.2" + glob "^13.0.6" + graceful-fs "^4.2.11" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^6.0.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^5.0.0" + istanbul-reports "^3.1.3" + jest-message-util "30.5.1" + jest-util "30.5.1" + jest-worker "30.5.1" + slash "^3.0.0" + string-length "^4.0.2" + v8-to-istanbul "^9.0.1" + +"@jest/schemas@30.5.0": + version "30.5.0" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-30.5.0.tgz#781f142de46345b903f43140b15865732abfd356" + integrity sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg== + dependencies: + "@sinclair/typebox" "^0.34.0" + +"@jest/snapshot-utils@30.5.1": + version "30.5.1" + resolved "https://registry.yarnpkg.com/@jest/snapshot-utils/-/snapshot-utils-30.5.1.tgz#172720d0546ab05d5a7dc7fa3feb74c166c96310" + integrity sha512-V3wnxNtiVmw5PPVg433Cn3VdXnsOeu/ofLw3KC04Bn2y1wIlU5kozXQn48rhrgj/02LrCVtntTEF1yBha0XSUw== + dependencies: + "@jest/types" "30.5.1" + chalk "^4.1.2" + graceful-fs "^4.2.11" + natural-compare "^1.4.0" + +"@jest/source-map@30.5.0": + version "30.5.0" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-30.5.0.tgz#7c3da7fafcffcc92c3605c15a7fcabba120e6f3d" + integrity sha512-xWpTJP9D0bDFGbPGT8XuWSwwha/iHADyyKzUnMx4UbdgnHugxrDaQFO4RZ8x4ZsFzRP6pNii8uvlgKCDxCIuDg== + dependencies: + "@jridgewell/trace-mapping" "^0.3.31" + callsites "^3.1.0" + convert-source-map "^2.0.0" + graceful-fs "^4.2.11" + +"@jest/test-result@30.5.1": + version "30.5.1" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-30.5.1.tgz#084d9221f157bdbe9f16c9ba9b1a83da7831a540" + integrity sha512-A/1S6ZBdpic50E0pxLgvaB9XNPL4k7AksmG69OO2oiotxciWZwHkbOec+qbIi+uUtIIByOVlXJUl97oZUsz+Jw== + dependencies: + "@jest/console" "30.5.1" + "@jest/types" "30.5.1" + "@types/istanbul-lib-coverage" "^2.0.6" + collect-v8-coverage "^1.0.2" + +"@jest/test-sequencer@30.5.1": + version "30.5.1" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-30.5.1.tgz#6c8d48bb957988ed38c82683e051089cc4929751" + integrity sha512-SHcPnrjdVRYJv6y6l4JUTy4Jccu7zmO6BmiOfOA34UiVBto22s19WiDC0A/2qfM7MWB/qdcoqfSIRMitpjTT4A== + dependencies: + "@jest/test-result" "30.5.1" + graceful-fs "^4.2.11" + jest-haste-map "30.5.1" + slash "^3.0.0" + +"@jest/transform@30.5.1": + version "30.5.1" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-30.5.1.tgz#563895f9cb60bc490c3addde758e78f3dd9dc321" + integrity sha512-EDnDhn0jleU9ZhpVoA4gvqw+Ev0iw/r5upNT2b79RiwiaTiYAMMhvNJP3lmocjIe5J2ZkoNr0B3K/J6O5GIm4Q== + dependencies: + "@babel/core" "^7.27.4" + "@jest/types" "30.5.1" + "@jridgewell/trace-mapping" "^0.3.31" + babel-plugin-istanbul "^8.0.0" + chalk "^4.1.2" + convert-source-map "^2.0.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.11" + jest-haste-map "30.5.1" + jest-regex-util "30.5.0" + jest-util "30.5.1" + pirates "^4.0.7" + slash "^3.0.0" + write-file-atomic "^5.0.1" + +"@jest/types@30.5.1": + version "30.5.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-30.5.1.tgz#dc08c773401c18ea0d9ca670fb4a10a22c8a4fb5" + integrity sha512-LvVYn83nnXPl+Rg98nvcFgjx6nRMTArhSn6RAX/w3ELn54S8A42TYZvsCMdGUqTM8S0wyXbtlQdU6Hi6dykj9g== + dependencies: + "@jest/pattern" "30.5.0" + "@jest/schemas" "30.5.0" + "@types/istanbul-lib-coverage" "^2.0.6" + "@types/istanbul-reports" "^3.0.4" + "@types/node" "*" + "@types/yargs" "^17.0.33" + chalk "^4.1.2" + +"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz#f4c663e862f06dc98ca4d453862c46902789a18d" + integrity sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.23", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28", "@jridgewell/trace-mapping@^0.3.31": + version "0.3.31" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@napi-rs/wasm-runtime@^1.1.4": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz#97e3d45d7424dc5da1d4e32f3bf3b292f6c1b44c" + integrity sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q== + dependencies: + "@tybys/wasm-util" "^0.10.3" + +"@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" + +"@parcel/watcher-android-arm64@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz#99aaa3223d43807c9340af439cad7e9b6d26ada6" + integrity sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA== + +"@parcel/watcher-darwin-arm64@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz#024496e586b4744f09ce532bbe89fe38ef02a64e" + integrity sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw== + +"@parcel/watcher-darwin-x64@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz#a4621df1359a93d39a332d9bab5ff09016a0608f" + integrity sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw== + +"@parcel/watcher-freebsd-x64@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz#7f565ed1a5b3a5e604e6a4799121518265d62a3d" + integrity sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ== + +"@parcel/watcher-linux-arm-glibc@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz#ad7d3825e67b81999165da42593022045abc0889" + integrity sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg== + +"@parcel/watcher-linux-arm-musl@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz#fe7d1cccb2c483215c090e938cf5cf404d2f9a8c" + integrity sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw== + +"@parcel/watcher-linux-arm64-glibc@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz#7e239dcb4646c4c79f006a7131a48238249530da" + integrity sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g== + +"@parcel/watcher-linux-arm64-musl@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz#c58b8d9c6d8d81594be00dd83aab741c1aaf7e0e" + integrity sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA== + +"@parcel/watcher-linux-x64-glibc@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz#5184fa9a770478d86e56875f4ee163a0abdc8791" + integrity sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A== + +"@parcel/watcher-linux-x64-musl@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz#2d1c55aa7246cbc7670e2612058a8a542c9cf246" + integrity sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw== + +"@parcel/watcher-win32-arm64@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz#15e09432040fee9e2213aa9c10ed589012526def" + integrity sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ== + +"@parcel/watcher-win32-x64@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz#9bee199a2a4accd557b451ac2c1c793f305ae012" + integrity sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A== + +"@parcel/watcher@^2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.6.0.tgz#99661f6220070b76a766aba6b7e313a087a1be4f" + integrity sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w== + dependencies: + detect-libc "^2.0.3" + is-glob "^4.0.3" + node-addon-api "^7.0.0" + picomatch "^4.0.4" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.6.0" + "@parcel/watcher-darwin-arm64" "2.6.0" + "@parcel/watcher-darwin-x64" "2.6.0" + "@parcel/watcher-freebsd-x64" "2.6.0" + "@parcel/watcher-linux-arm-glibc" "2.6.0" + "@parcel/watcher-linux-arm-musl" "2.6.0" + "@parcel/watcher-linux-arm64-glibc" "2.6.0" + "@parcel/watcher-linux-arm64-musl" "2.6.0" + "@parcel/watcher-linux-x64-glibc" "2.6.0" + "@parcel/watcher-linux-x64-musl" "2.6.0" + "@parcel/watcher-win32-arm64" "2.6.0" + "@parcel/watcher-win32-x64" "2.6.0" + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@pkgr/core@^0.3.6": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.3.6.tgz#3569708bd4be4d8870ba32bf1c456dac81600d97" + integrity sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA== + +"@redis/bloom@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-5.12.1.tgz#047dbfce93cfa7e5879fb58ad1b2afe87ebb2dc4" + integrity sha512-PUUfv+ms7jgPSBVoo/DN4AkPHj4D5TZSd6SbJX7egzBplkYUcKmHRE8RKia7UtZ8bSQbLguLvxVO+asKtQfZWA== + +"@redis/client@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@redis/client/-/client-5.12.1.tgz#a35a2bac546c727d7915d2d91b63a77111e51ebd" + integrity sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA== + dependencies: + cluster-key-slot "1.1.2" + +"@redis/json@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@redis/json/-/json-5.12.1.tgz#52aff987abe4d41ec9644857fb00f16c7a97fdb7" + integrity sha512-eOze75esLve4vfqDel7aMX08CNaiLLQS2fV8mpRN9NxPe1rVR4vQyYiW/OgtGUysF6QOr9ANhfxABKNOJfXdKg== + +"@redis/search@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@redis/search/-/search-5.12.1.tgz#b7a738be918c8a7af91e39c5bd2023f30f392981" + integrity sha512-ItlxbxC9cKI6IU1TLWoczwJCRb6TdmkEpWv05UrPawqaAnWGRu3rcIqsc5vN483T2fSociuyV1UkWIL5I4//2w== + +"@redis/time-series@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-5.12.1.tgz#15b6deaaf3716bc2633311c0ed18201c9299392d" + integrity sha512-c6JL6E3EcZJuNqKFz+KM+l9l5mpcQiKvTwgA3blt5glWJ8hjDk0yeHN3beE/MpqYIQ8UEX44ItQzgkE/gCBELQ== + +"@sinclair/typebox@^0.34.0": + version "0.34.52" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.34.52.tgz#62f8a686e4ab28a8944902e2ad2d648312ef11cb" + integrity sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw== + +"@sinonjs/commons@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" + integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^15.4.0": + version "15.4.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz#5d40c151a9e66075fe4520bec40bccfe54931962" + integrity sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA== + dependencies: + "@sinonjs/commons" "^3.0.1" + +"@tsconfig/node10@^1.0.7": + version "1.0.13" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.13.tgz#56105a9a8c786e8f15e35746879cf2d52275485b" + integrity sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + +"@tybys/wasm-util@^0.10.3": + version "0.10.3" + resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz#015cba9e9dd47ce14d03d2a8c5d547bfb169665d" + integrity sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg== + dependencies: + tslib "^2.4.0" + +"@types/babel__core@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.27.0.tgz#b5819294c51179957afaec341442f9341e4108a9" + integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz#07d713d6cce0d265c9849db0cbe62d3f61f36f74" + integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q== + dependencies: + "@babel/types" "^7.28.2" + +"@types/body-parser@*": + version "1.19.6" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474" + integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + 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.17": + version "2.8.19" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.19.tgz#d93ea2673fd8c9f697367f5eeefc2bbfa94f0342" + integrity sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@^5.0.0": + version "5.1.3" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz#9d34c88c0c9ee62b9a6e4d9f8ab8d7e29688e6b4" + integrity sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@^5.0.5": + version "5.0.6" + resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.6.tgz#2d724b2c990dcb8c8444063f3580a903f6d500cc" + integrity sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^5.0.0" + "@types/serve-static" "^2" + +"@types/http-errors@*": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" + integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.1", "@types/istanbul-lib-coverage@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" + integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== + +"@types/istanbul-lib-report@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf" + integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" + integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@^30.0.0": + version "30.0.0" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-30.0.0.tgz#5e85ae568006712e4ad66f25433e9bdac8801f1d" + integrity sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA== + dependencies: + expect "^30.0.0" + pretty-format "^30.0.0" + +"@types/jsonwebtoken@^9.0.10": + version "9.0.10" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz#a7932a47177dcd4283b6146f3bd5c26d82647f09" + integrity sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA== + dependencies: + "@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" + integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== + +"@types/node@*": + version "26.4.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-26.4.1.tgz#3d8dc80515894958448ee266cf5d6bc3e5205bd5" + integrity sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA== + dependencies: + undici-types "~8.3.0" + +"@types/node@^14.0.1": + version "14.18.63" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b" + integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ== + +"@types/node@^24.10.0": + version "24.13.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.13.3.tgz#49f18bd3c647866dcda51a0756c145e14590ce16" + integrity sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q== + dependencies: + undici-types "~7.18.0" + +"@types/qs@*": + version "6.15.1" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.15.1.tgz#8606884272c63f0db96986bd3548650d8a9388bf" + integrity sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/send@*": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@types/send/-/send-1.2.1.tgz#6a784e45543c18c774c049bff6d3dbaf045c9c74" + integrity sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ== + dependencies: + "@types/node" "*" + +"@types/serve-static@^2": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-2.2.0.tgz#d4a447503ead0d1671132d1ab6bd58b805d8de6a" + integrity sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + +"@types/stack-utils@^2.0.3": + version "2.0.3" + 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.11" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-8.1.11.tgz#14da75aa2f916dcdd6fb2a90a8fb24a9a1a86d08" + integrity sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw== + 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/yargs-parser@*": + version "21.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" + integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== + +"@types/yargs@^17.0.33": + version "17.0.35" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.35.tgz#07013e46aa4d7d7d50a49e15604c1c5340d4eb24" + integrity sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg== + dependencies: + "@types/yargs-parser" "*" + +"@ungap/structured-clone@^1.3.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.4.0.tgz#5e2e1374c0a30b5a42e8b083523a225c6945f88a" + integrity sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ== + +"@unrs/resolver-binding-android-arm-eabi@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz#98a9fee62c01f209747a4ab5855f1ced38a6d03a" + integrity sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w== + +"@unrs/resolver-binding-android-arm64@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz#46b7e8a1393f907462324f1576e8883529acf066" + integrity sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ== + +"@unrs/resolver-binding-darwin-arm64@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz#0ea07b00e2583ab004b853d4c02ec5f0745d490c" + integrity sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w== + +"@unrs/resolver-binding-darwin-x64@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz#a2a6901ed58449b91b4438e582f6890cba956049" + integrity sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA== + +"@unrs/resolver-binding-freebsd-x64@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz#ebe6fe7f6706b7378ea4a48a024602e9c2f48f89" + integrity sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg== + +"@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz#e6040fedaa240124419d35b25b69c5fa15ddb499" + integrity sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A== + +"@unrs/resolver-binding-linux-arm-musleabihf@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz#d217a8fb59f659c131539326c140e7b62e3e3c6a" + integrity sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g== + +"@unrs/resolver-binding-linux-arm64-gnu@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz#edab13c46a45783a7e01351e113825c04f352e24" + integrity sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg== + +"@unrs/resolver-binding-linux-arm64-musl@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz#e5e195db1130f7d3b6aa2fd67b3c9fe1ea4859a0" + integrity sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA== + +"@unrs/resolver-binding-linux-loong64-gnu@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz#f01d22e091bae13016f4636698d9dcbbda775c3e" + integrity sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q== + +"@unrs/resolver-binding-linux-loong64-musl@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz#7d23efcb98adf076bfbcecc27b4212c36aa6697d" + integrity sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew== + +"@unrs/resolver-binding-linux-ppc64-gnu@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz#1f35f1eaa322f33cf2d96dac27f0626a93ffe2f6" + integrity sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg== + +"@unrs/resolver-binding-linux-riscv64-gnu@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz#674faa696f5ce96f214873946a1e2d6ca96723dd" + integrity sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A== + +"@unrs/resolver-binding-linux-riscv64-musl@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz#37835fdd0b472ecdcffccd4288f19018454b138c" + integrity sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w== + +"@unrs/resolver-binding-linux-s390x-gnu@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz#b6edf13db4bb0accdcd1ad482a4eea0301de9224" + integrity sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw== + +"@unrs/resolver-binding-linux-x64-gnu@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz#daddad00bf65a405202284da1eb1db8eb83b218f" + integrity sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ== + +"@unrs/resolver-binding-linux-x64-musl@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz#dfdff1e0c2bad25420b41c76a746011c3983b9bb" + integrity sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A== + +"@unrs/resolver-binding-openharmony-arm64@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz#ce07c4f5e7b42f7bfce45e7629b8659063aefefe" + integrity sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ== + +"@unrs/resolver-binding-wasm32-wasi@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz#82514f0506cfaf65f17fe16095f92d450e487183" + integrity sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A== + dependencies: + "@emnapi/core" "1.10.0" + "@emnapi/runtime" "1.10.0" + "@napi-rs/wasm-runtime" "^1.1.4" + +"@unrs/resolver-binding-win32-arm64-msvc@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz#521427dd59a8f4740ddd1dc7c3bc6af1aa1d260d" + integrity sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g== + +"@unrs/resolver-binding-win32-ia32-msvc@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz#05b63286ff2da37e0ce3083b8390884385efff62" + integrity sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g== + +"@unrs/resolver-binding-win32-x64-msvc@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz#72da0da48d72b1e87831b9c0308931d3f4669027" + integrity sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA== + +accepts@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" + integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== + dependencies: + mime-types "^3.0.0" + negotiator "^1.0.0" + +acorn-walk@^8.1.1: + version "8.3.5" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.5.tgz#8a6b8ca8fc5b34685af15dabb44118663c296496" + integrity sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw== + dependencies: + acorn "^8.11.0" + +acorn@^8.11.0, acorn@^8.4.1: + version "8.18.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.18.0.tgz#4faf01b2d6d326bfeed97aea1f52220b5f4c1940" + integrity sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ== + +ansi-escapes@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.2.2: + version "6.3.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.3.0.tgz#247c8e7b70a1a43b10ce14c0226fcbf58e8815d5" + integrity sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +ansi-styles@^6.1.0: + version "6.2.3" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.3.tgz#c044d5dcc521a076413472597a1acb1f103c4041" + integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== + +anymatch@^3.1.3, anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +archiver-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2" + integrity sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw== + dependencies: + glob "^7.1.4" + graceful-fs "^4.2.0" + lazystream "^1.0.0" + lodash.defaults "^4.2.0" + lodash.difference "^4.5.0" + lodash.flatten "^4.4.0" + lodash.isplainobject "^4.0.6" + lodash.union "^4.6.0" + normalize-path "^3.0.0" + readable-stream "^2.0.0" + +archiver-utils@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-3.0.4.tgz#a0d201f1cf8fce7af3b5a05aea0a337329e96ec7" + integrity sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw== + dependencies: + glob "^7.2.3" + graceful-fs "^4.2.0" + lazystream "^1.0.0" + lodash.defaults "^4.2.0" + lodash.difference "^4.5.0" + lodash.flatten "^4.4.0" + lodash.isplainobject "^4.0.6" + lodash.union "^4.6.0" + normalize-path "^3.0.0" + readable-stream "^3.6.0" + +archiver@^5.0.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.2.tgz#99991d5957e53bd0303a392979276ac4ddccf3b0" + integrity sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw== + dependencies: + archiver-utils "^2.1.0" + async "^3.2.4" + buffer-crc32 "^0.2.1" + readable-stream "^3.6.0" + readdir-glob "^1.1.2" + tar-stream "^2.2.0" + zip-stream "^4.1.0" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + 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== + +async@^3.2.4: + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +babel-jest@30.5.1, babel-jest@^30.2.0: + version "30.5.1" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-30.5.1.tgz#b20da241e3d525e17a1cda61eadbc2e13230baeb" + integrity sha512-ge1xUVZS91ml09YRMgRGgeKJ4YJpcOiuwteAxFBYLugQyp7cRw+hHej6Ho0vPjvLrjq60bb7JPHH9LAMA1/krA== + dependencies: + "@jest/transform" "30.5.1" + "@types/babel__core" "^7.20.5" + babel-plugin-istanbul "^8.0.0" + babel-preset-jest "30.5.0" + chalk "^4.1.2" + graceful-fs "^4.2.11" + slash "^3.0.0" + +babel-plugin-istanbul@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-8.0.0.tgz#8762f542153a52b77e626dd2c033b467de2f2fa2" + integrity sha512-18wCskrN3DgbuBmp1gr7LBGT8xdz5xhQQqFvFhVxbkl8VBCrMKQ2YtqBWtUal1Zrc1HTuX0011+Brjw78TCFkg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.3" + istanbul-lib-instrument "^6.0.2" + test-exclude "^7.0.1" + +babel-plugin-jest-hoist@30.5.0: + version "30.5.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.5.0.tgz#425bf7ee24cffe47bfdcfeeca2885b87b1cf6644" + integrity sha512-gtGo1B+u14jrZQv6TdSWIWkTqclboo7Qn+dFAGUOIuXLFuJKAdg3U5MIWzZnW4vfUb4dX9skmAMiby86e/SF4A== + dependencies: + "@types/babel__core" "^7.20.5" + +babel-plugin-polyfill-corejs2@^0.4.15: + version "0.4.17" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz#198f970f1c99a856b466d1187e88ce30bd199d91" + integrity sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w== + dependencies: + "@babel/compat-data" "^7.28.6" + "@babel/helper-define-polyfill-provider" "^0.6.8" + semver "^6.3.1" + +babel-plugin-polyfill-corejs3@^0.14.0: + version "0.14.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz#6ac08d2f312affb70c4c69c0fbba4cb417ee5587" + integrity sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.8" + core-js-compat "^3.48.0" + +babel-plugin-polyfill-regenerator@^0.6.6: + version "0.6.8" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz#8a6bfd5dd54239362b3d06ce47ac52b2d95d7721" + integrity sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.8" + +babel-preset-current-node-syntax@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz#20730d6cdc7dda5d89401cab10ac6a32067acde6" + integrity sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-import-attributes" "^7.24.7" + "@babel/plugin-syntax-import-meta" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + +babel-preset-jest@30.5.0: + version "30.5.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-30.5.0.tgz#33162eee375c0066f2b2059cd97a7840ef84c259" + integrity sha512-ZGPn5ClP4lBDpuOK8W1yQIOy359HmbnZv3suucAlIe+SEE9yDsxV4S2PjSbH2Vc97U+WmzYD7vw3kr8NsQ/i6w== + dependencies: + babel-plugin-jest-hoist "30.5.0" + babel-preset-current-node-syntax "^1.2.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +balanced-match@^4.0.2: + version "4.0.4" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a" + integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +baseline-browser-mapping@^2.11.20: + version "2.11.21" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz#99af73cb8e54007e4f5345e132278e26c2662f2c" + integrity sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ== + +big-integer@^1.6.17: + version "1.6.52" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" + integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +binary@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" + integrity sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg== + dependencies: + buffers "~0.1.1" + chainsaw "~0.1.0" + +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +bluebird@~3.4.1: + version "3.4.7" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" + integrity sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA== + +body-parser@^2.2.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.3.0.tgz#6d8662f4d8c336028b8ac9aa24251b0ca64ba437" + integrity sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw== + dependencies: + bytes "^3.1.2" + content-type "^2.0.0" + debug "^4.4.3" + http-errors "^2.0.1" + iconv-lite "^0.7.2" + on-finished "^2.4.1" + qs "^6.15.2" + raw-body "^3.0.2" + type-is "^2.1.0" + +brace-expansion@^1.1.7: + version "1.1.18" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.18.tgz#3ce74d89885136be1535341f8c3d4425c29a5cab" + integrity sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1, brace-expansion@^2.0.2: + version "2.1.4" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.1.4.tgz#589dab11c0018d0366be64cd8bf12c8dbecc8326" + integrity sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg== + dependencies: + balanced-match "^1.0.0" + +brace-expansion@^5.0.8: + version "5.0.9" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.9.tgz#7c72438809b5fa5babf54199a1f1c281a6984fcf" + integrity sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg== + dependencies: + balanced-match "^4.0.2" + +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browserslist@^4.24.0, browserslist@^4.28.7: + version "4.28.9" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.9.tgz#07ce6b449b90af880eb9bfb7cd39372cc4f71c8c" + integrity sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg== + dependencies: + baseline-browser-mapping "^2.11.20" + caniuse-lite "^1.0.30001810" + electron-to-chromium "^1.5.420" + node-releases "^2.0.54" + update-browserslist-db "^1.3.2" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-crc32@^0.2.1, buffer-crc32@^0.2.13: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + +buffer-equal-constant-time@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + +buffer-indexof-polyfill@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz#d2732135c5999c64b277fcf9b1abe3498254729c" + integrity sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A== + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +buffers@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" + integrity sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ== + +bytes@^3.1.2, bytes@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + +callsites@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001810: + version "1.0.30001810" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz#4970b477dea3278374de9bc43aa8f5d39fc3cda2" + integrity sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg== + +chainsaw@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" + integrity sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ== + dependencies: + traverse ">=0.3.0 <0.4" + +chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +chokidar@^3.5.2: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +ci-info@^4.2.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.4.0.tgz#7d54eff9f54b45b62401c26032696eb59c8bd18c" + integrity sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg== + +cjs-module-lexer@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-2.2.1.tgz#ab35b03c56ade05fe170c70e67ae89f60666847c" + integrity sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q== + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +cluster-key-slot@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" + integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + +collect-v8-coverage@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz#cc1f01eb8d02298cbc9a437c74c70ab4e5210b80" + integrity sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw== + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + 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== + +compress-commons@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.2.tgz#6542e59cb63e1f46a8b21b0e06f9a32e4c8b06df" + integrity sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg== + dependencies: + buffer-crc32 "^0.2.13" + crc32-stream "^4.0.2" + normalize-path "^3.0.0" + readable-stream "^3.6.0" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +content-disposition@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.1.0.tgz#f3db789c752d45564cc7e9e1e0b31790d4a38e17" + integrity sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g== + +content-type@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +content-type@^2.0.0, content-type@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-2.1.0.tgz#d9389c43c0a8cf6a355db464d21e07092a40493a" + integrity sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag== + +convert-source-map@^2.0.0: + version "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.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" + integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== + +cookie@^0.7.1: + version "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.48.0: + version "3.50.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.50.0.tgz#d5922c2a692ab1cba6078c920e7c5567421ae08e" + integrity sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q== + dependencies: + browserslist "^4.28.7" + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@^2.8.5: + version "2.8.6" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.6.tgz#ff5dd69bd95e547503820d29aba4f8faf8dfec96" + integrity sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw== + dependencies: + object-assign "^4" + vary "^1" + +crc-32@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" + integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== + +crc32-stream@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-4.0.3.tgz#85dd677eb78fa7cad1ba17cc506a597d41fc6f33" + integrity sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw== + dependencies: + crc-32 "^1.2.0" + readable-stream "^3.4.0" + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cross-spawn@^7.0.3, cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +dayjs@^1.8.34: + version "1.11.23" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.23.tgz#b0a363506dde5f36cf5075e42ebe8115165a8c79" + integrity sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ== + +debug@^4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.7, debug@^4.4.0, debug@^4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +dedent@^1.6.0: + version "1.7.2" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.7.2.tgz#34e2264ab538301e27cf7b07bf2369c19baa8dd9" + integrity sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA== + +deepmerge@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@^2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +detect-libc@^2.0.3: + version "2.1.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + +detect-newline@^3.1.0: + version "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.4" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.4.tgz#7a6dbfda325f25f07517e9b518f897c08332e07d" + integrity sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ== + +dotenv@^17.2.3: + version "17.4.2" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-17.4.2.tgz#c07e54a746e11eba021dd9e1047ced5afdc1c034" + integrity sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw== + +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +duplexer2@~0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + integrity sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA== + dependencies: + readable-stream "^2.0.2" + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.5.420: + version "1.5.422" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.422.tgz#e27fb1ca0e6bef612a647022e1469701fea9ee1f" + integrity sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA== + +emittery@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" + integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +encodeurl@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +end-of-stream@^1.4.1: + version "1.4.5" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c" + integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg== + dependencies: + once "^1.4.0" + +error-ex@^1.3.1: + version "1.3.4" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414" + integrity sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ== + dependencies: + is-arrayish "^0.2.1" + +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-module-lexer@^2.1.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-2.3.2.tgz#311fa4f40168c1975c505477c51b23234d41ad55" + integrity sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz#a2d0b373205724dfa525d23b0c3e1b1ca582c99b" + integrity sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +escalade@^3.1.1, escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-html@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +exceljs@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/exceljs/-/exceljs-4.4.0.tgz#cfb1cb8dcc82c760a9fc9faa9e52dadab66b0156" + integrity sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg== + dependencies: + archiver "^5.0.0" + dayjs "^1.8.34" + fast-csv "^4.3.1" + jszip "^3.10.1" + readable-stream "^3.6.0" + saxes "^5.0.1" + tmp "^0.2.0" + unzipper "^0.10.11" + uuid "^8.3.0" + +execa@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +exit-x@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/exit-x/-/exit-x-0.2.2.tgz#1f9052de3b8d99a696b10dad5bced9bdd5c3aa64" + integrity sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ== + +expect@30.5.1, expect@^30.0.0: + version "30.5.1" + resolved "https://registry.yarnpkg.com/expect/-/expect-30.5.1.tgz#0fdbaf8a9be4c660f394d745fc17174d08b4fba6" + integrity sha512-m8YrYgvKe9+9gEnWEuKz+qCGfHqkrff7PPfyDnOFkjsfYRKqiYyOxDFMFieCGAuhtk/VNm63tvNgKZVzVy+Hvg== + dependencies: + "@jest/expect-utils" "30.5.1" + "@jest/get-type" "30.5.0" + jest-matcher-utils "30.5.1" + jest-message-util "30.5.1" + jest-mock "30.5.1" + jest-util "30.5.1" + +express@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/express/-/express-5.2.1.tgz#8f21d15b6d327f92b4794ecf8cb08a72f956ac04" + integrity sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw== + 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" + +fast-csv@^4.3.1: + version "4.3.6" + resolved "https://registry.yarnpkg.com/fast-csv/-/fast-csv-4.3.6.tgz#70349bdd8fe4d66b1130d8c91820b64a21bc4a63" + integrity sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw== + dependencies: + "@fast-csv/format" "4.3.5" + "@fast-csv/parse" "4.3.6" + +fast-json-stable-stringify@^2.1.0: + version "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" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== + dependencies: + bser "2.1.1" + +fdir@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.1.1.tgz#a2c517a6559852bcdb06d1f8bd7f51b68fad8099" + integrity sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA== + 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" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +foreground-child@^3.1.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" + integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== + dependencies: + cross-spawn "^7.0.6" + signal-exit "^4.0.1" + +form-data@^4.0.0, form-data@^4.0.5: + version "4.0.6" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.6.tgz#28e864e1b786dbebb68db1f452f9635278665827" + integrity sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + hasown "^2.0.4" + mime-types "^2.1.35" + +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" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4" + integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +fstream@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" + integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + 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" + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@^10.4.1: + version "10.5.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c" + integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + +glob@^13.0.6: + version "13.0.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-13.0.6.tgz#078666566a425147ccacfbd2e332deb66a2be71d" + integrity sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw== + dependencies: + minimatch "^10.2.2" + minipass "^7.1.3" + path-scurry "^2.0.2" + +glob@^7.1.3, glob@^7.1.4, glob@^7.2.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +graceful-fs@^4.1.2, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.2: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.2, hasown@^2.0.3, hasown@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.4.tgz#8c62d8cb90beb2aad5d0a5b67581ad9854c3f003" + integrity sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A== + dependencies: + function-bind "^1.1.2" + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +http-errors@^2.0.0, http-errors@^2.0.1, http-errors@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b" + integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ== + dependencies: + depd "~2.0.0" + inherits "~2.0.4" + setprototypeof "~1.2.0" + statuses "~2.0.2" + toidentifier "~1.0.1" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@^0.7.2, iconv-lite@~0.7.0: + version "0.7.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.7.3.tgz#84ee12f963e7de50bc01a13e160a078b3b0f415f" + integrity sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + +import-local@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3, inherits@~2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.16.1: + version "2.16.2" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.2.tgz#3e07450a8080ebce3fbf0cac494f4d2ab324e082" + integrity sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA== + dependencies: + hasown "^2.0.3" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-promise@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" + integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== + +istanbul-lib-instrument@^6.0.0, istanbul-lib-instrument@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz#fa15401df6c15874bcb2105f773325d78c666765" + integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q== + dependencies: + "@babel/core" "^7.23.9" + "@babel/parser" "^7.23.9" + "@istanbuljs/schema" "^0.1.3" + istanbul-lib-coverage "^3.2.0" + semver "^7.5.4" + +istanbul-lib-report@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" + integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^4.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^5.0.0: + version "5.0.6" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz#acaef948df7747c8eb5fbf1265cb980f6353a441" + integrity sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A== + dependencies: + "@jridgewell/trace-mapping" "^0.3.23" + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + +istanbul-reports@^3.1.3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz#cb4535162b5784aa623cee21a7252cf2c807ac93" + integrity sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +jest-changed-files@30.5.1: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-30.5.1.tgz#0f91cc77e6f834fb2e85bd277d9b313e555acd43" + integrity sha512-0+bvMM/ENhDI29Z8q1r4HxiDIi4G5tnBmSw4esfPQoj9q8Nik7KIyuxHkTVnnniJQf05SxpGaKDQ+4h28ynGPg== + dependencies: + execa "^5.1.1" + jest-util "30.5.1" + p-limit "^3.1.0" + +jest-circus@30.5.1: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-30.5.1.tgz#323ff2a51a656958acbb2fe983702261eee71981" + integrity sha512-NgliezXQ6yznqR4W5Gqw++0cZSbBZlF0NknNIAE5VmSJKWOtmWJmWnBq3TSkUOVBTPrT7FGGhVdA8DLWnxo2Sw== + dependencies: + "@jest/environment" "30.5.1" + "@jest/expect" "30.5.1" + "@jest/test-result" "30.5.1" + "@jest/types" "30.5.1" + "@types/node" "*" + chalk "^4.1.2" + co "^4.6.0" + dedent "^1.6.0" + is-generator-fn "^2.1.0" + jest-each "30.5.1" + jest-matcher-utils "30.5.1" + jest-message-util "30.5.1" + jest-runtime "30.5.1" + jest-snapshot "30.5.1" + jest-util "30.5.1" + p-limit "^3.1.0" + pretty-format "30.5.1" + pure-rand "^7.0.0" + slash "^3.0.0" + stack-utils "^2.0.6" + +jest-cli@30.5.1: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-30.5.1.tgz#5462c51d01b41e8f339faa88fd9139eeb0a9b34d" + integrity sha512-uwNYepWgaNBCplm42fCIUZTf/tIEHIy5AYvx7eL1BrwIgyjPt+2poouR23UO2yopIP+kV8oZFHfv+l4pg/8prQ== + dependencies: + "@jest/core" "30.5.1" + "@jest/test-result" "30.5.1" + "@jest/types" "30.5.1" + chalk "^4.1.2" + exit-x "^0.2.2" + import-local "^3.2.0" + jest-config "30.5.1" + jest-util "30.5.1" + jest-validate "30.5.1" + yargs "^17.7.2" + +jest-config@30.5.1: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-30.5.1.tgz#fff038a74c4750c478efe3a66bde8c80051edb48" + integrity sha512-L8PKM2X/ngG8PxfLMqglKGZjylPgw84bVPUlMY9W/o76TnLqPWrmQgsaT0HrheGdRtpGE5FbmREA0Kvb+Qx5gQ== + dependencies: + "@babel/core" "^7.27.4" + "@jest/get-type" "30.5.0" + "@jest/pattern" "30.5.0" + "@jest/test-sequencer" "30.5.1" + "@jest/types" "30.5.1" + babel-jest "30.5.1" + chalk "^4.1.2" + ci-info "^4.2.0" + deepmerge "^4.3.1" + glob "^13.0.6" + graceful-fs "^4.2.11" + jest-circus "30.5.1" + jest-docblock "30.5.0" + jest-environment-node "30.5.1" + jest-regex-util "30.5.0" + jest-resolve "30.5.1" + jest-runner "30.5.1" + jest-util "30.5.1" + jest-validate "30.5.1" + parse-json "^5.2.0" + pretty-format "30.5.1" + slash "^3.0.0" + strip-json-comments "^3.1.1" + +jest-diff@30.5.1: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-30.5.1.tgz#e03474eca7e5dc42924b15c72069b3203253768d" + integrity sha512-e3cNNMpv8Kh20MjjphTXs+3Vz7DQyLM1nft7KJhnh46atFhjVJRa+0Hq0beywuwsACtMQUBihQkFl8zxb7gt1Q== + dependencies: + "@jest/diff-sequences" "30.5.0" + "@jest/get-type" "30.5.0" + chalk "^4.1.2" + pretty-format "30.5.1" + +jest-docblock@30.5.0: + version "30.5.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-30.5.0.tgz#7cf9d08ba714fde0b68c9cb4bfc0be867a7dd11d" + integrity sha512-NwDqcxtoZi33RhuW+zJS/RVA3rmheQ8BnwpYZuc/Eruaz6seQb7+aoCeDZu/3X7W2XmD8DbSo9Pn72DbKsBFYw== + dependencies: + detect-newline "^3.1.0" + +jest-each@30.5.1: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-30.5.1.tgz#6647c948db58351d4081851eae329a410288bacd" + integrity sha512-S1af0TU4v1EZ/AUlkFs/sxf/5KGsbAT9kRgdyoMX/x72y7C8ZEgETE5o1TPnbKRnrbprc5FR/moYLfdRmqEjsQ== + dependencies: + "@jest/get-type" "30.5.0" + "@jest/types" "30.5.1" + chalk "^4.1.2" + jest-util "30.5.1" + pretty-format "30.5.1" + +jest-environment-node@30.5.1: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-30.5.1.tgz#c4581f0a0b656d2b17be809066c051cee99ea8ed" + integrity sha512-LrPj3sPMjsQoOB3jrb8p/sa+XkSFNKo60TTsyc+EB2kxQJHgbwElpXnx1yX25fdFn5958FIObRtcLSHyV8VIAw== + dependencies: + "@jest/environment" "30.5.1" + "@jest/fake-timers" "30.5.1" + "@jest/types" "30.5.1" + "@types/node" "*" + jest-mock "30.5.1" + jest-util "30.5.1" + jest-validate "30.5.1" + +jest-haste-map@30.5.1: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-30.5.1.tgz#a1dbba63564492f53783d754623cdd41bb0328b0" + integrity sha512-VIFgt67jW480YDxKfEv9IYQKrFpYt7bOCMn3VsnjK7AK9qo7p6acpnA1DHwsVOULE3dTaYew/6JaTD/d0VDHoQ== + dependencies: + "@jest/types" "30.5.1" + "@parcel/watcher" "^2.6.0" + "@types/node" "*" + anymatch "^3.1.3" + fb-watchman "^2.0.2" + fdir "^6.5.0" + graceful-fs "^4.2.11" + jest-regex-util "30.5.0" + jest-util "30.5.1" + jest-worker "30.5.1" + picomatch "^4.0.3" + +jest-leak-detector@30.5.1: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-30.5.1.tgz#bca4b6b08d5e3a0b46560e268d28c89b15e7b72e" + integrity sha512-gt4GT2aWgEoCNTcBe4rqS74xTIUJxi+UD9SNSu9aOk5LmTEd6fS5KSTmAKAfitCCktQHNN+upUuL6EkBfFbMDQ== + dependencies: + "@jest/get-type" "30.5.0" + pretty-format "30.5.1" + +jest-matcher-utils@30.5.1: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-30.5.1.tgz#93e64fa4362c44d68cdc7a5590a2ccf6972083f4" + integrity sha512-aroZVqwOz/wC2y6pC+obgFWKV9viaQWQTTSB6W5H55+egtUczIfXR/rxExTv92xD/ADYwpqaYfVWx1aqwKg7FA== + dependencies: + "@jest/get-type" "30.5.0" + chalk "^4.1.2" + jest-diff "30.5.1" + pretty-format "30.5.1" + +jest-message-util@30.5.1: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-30.5.1.tgz#e8d04d7b6d123f5dbfb1497432cd9c2b3d510f90" + integrity sha512-UdQlLdd9wL/Ys7xRErckqwD6wPlSZYueosSWuHc1r2ztGLwlgPvtSJq2+BPEgaEY13WLvfFbmhTj8pba0Sd1jg== + dependencies: + "@babel/code-frame" "^7.27.1" + "@jest/types" "30.5.1" + "@types/stack-utils" "^2.0.3" + chalk "^4.1.2" + graceful-fs "^4.2.11" + jest-util "30.5.1" + picomatch "^4.0.3" + pretty-format "30.5.1" + slash "^3.0.0" + stack-utils "^2.0.6" + +jest-mock@30.5.1: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-30.5.1.tgz#a52a7286d4bbf049bf9dabaa94616a9cc28c4b84" + integrity sha512-9fVjc3leUpGID2/by/LU4Dvdcp7PFh9LlxS3QRWK3ABm+KtvEVsG/AEGeLY3gKOZsjkBxyfwGltoAVlW7dygHg== + dependencies: + "@jest/expect-utils" "30.5.1" + "@jest/types" "30.5.1" + "@types/node" "*" + jest-util "30.5.1" + +jest-regex-util@30.5.0: + version "30.5.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-30.5.0.tgz#aedb1932d361d4e701ecacda6ac83acf37299505" + integrity sha512-Mg0WK7A6xRHLSA1udJ8y9f3lM0uUhFTBnLKzwPmqB9AylvpleJ6BLemR8K9dK27DY+cesDryoA7yLZCAHsPG1A== + +jest-resolve-dependencies@30.5.1: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-30.5.1.tgz#2ac3052a773e70277607609ba27351025cb4bb11" + integrity sha512-JKpXGONDcTaunrNVn8KCl6qAnwl06jIkvv70lhq0Ze47lsPx9sH5HoeEUiXX6iFGnliHN/qpIbQ0l38wrVmXaw== + dependencies: + jest-regex-util "30.5.0" + jest-snapshot "30.5.1" + +jest-resolve@30.5.1: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-30.5.1.tgz#929372ea827696ceb710eed4168999d0dd2152ed" + integrity sha512-wprhLejRtwN6h8ZgaqC0eYjGJ1uMdGPT/+b3eFncbG4NWuuP9QL+vWwRinu9waw7hkOLcpMJGVJAMgBwsPssMQ== + dependencies: + chalk "^4.1.2" + graceful-fs "^4.2.11" + jest-haste-map "30.5.1" + jest-util "30.5.1" + jest-validate "30.5.1" + slash "^3.0.0" + unrs-resolver "^1.12.1" + +jest-runner@30.5.1: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-30.5.1.tgz#2c2bc32ae71d7be4090bcd03b5018fc1b9733cc6" + integrity sha512-FPlQE4+mwnFxXmpPrSi836KV2ZzvK1g6/nPCT8o5BcoDUUNJQeHo1/Qdkoe/QeoL4M7OeJpbnGUhYKhC1VMdaQ== + dependencies: + "@jest/console" "30.5.1" + "@jest/environment" "30.5.1" + "@jest/source-map" "30.5.0" + "@jest/test-result" "30.5.1" + "@jest/transform" "30.5.1" + "@jest/types" "30.5.1" + "@types/node" "*" + chalk "^4.1.2" + emittery "^0.13.1" + exit-x "^0.2.2" + graceful-fs "^4.2.11" + jest-docblock "30.5.0" + jest-environment-node "30.5.1" + jest-haste-map "30.5.1" + jest-leak-detector "30.5.1" + jest-message-util "30.5.1" + jest-resolve "30.5.1" + jest-runtime "30.5.1" + jest-util "30.5.1" + jest-watcher "30.5.1" + jest-worker "30.5.1" + p-limit "^3.1.0" + +jest-runtime@30.5.1: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-30.5.1.tgz#76372008c88d124ad832e2f759ad280a92cca634" + integrity sha512-UB88+NRkK2Tw/OqV7dofYcyiUGrVZtD41k/N0xQOs9fG//57XHK7JLWG52HD1UYpUAhjK4xm6DBvFX3yWudsMA== + dependencies: + "@jest/environment" "30.5.1" + "@jest/fake-timers" "30.5.1" + "@jest/globals" "30.5.1" + "@jest/source-map" "30.5.0" + "@jest/test-result" "30.5.1" + "@jest/transform" "30.5.1" + "@jest/types" "30.5.1" + "@types/node" "*" + chalk "^4.1.2" + cjs-module-lexer "^2.2.0" + collect-v8-coverage "^1.0.2" + es-module-lexer "^2.1.0" + glob "^13.0.6" + graceful-fs "^4.2.11" + jest-haste-map "30.5.1" + jest-message-util "30.5.1" + jest-mock "30.5.1" + jest-regex-util "30.5.0" + jest-resolve "30.5.1" + jest-snapshot "30.5.1" + jest-util "30.5.1" + slash "^3.0.0" + strip-bom "^4.0.0" + +jest-snapshot@30.5.1: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-30.5.1.tgz#37fe8f798710bc1b9f598d5f51313c0c38438eae" + integrity sha512-cNWFdSb5xuDGl8hKkAZJ3YtI/PzHpAPFV+HUXWIOG8rMhpDTLVbvAc2d2wRicoYw2wGsJGLaurJT6BLC97bLXQ== + dependencies: + "@babel/core" "^7.27.4" + "@babel/generator" "^7.27.5" + "@babel/plugin-syntax-jsx" "^7.27.1" + "@babel/plugin-syntax-typescript" "^7.27.1" + "@babel/types" "^7.27.3" + "@jest/expect-utils" "30.5.1" + "@jest/get-type" "30.5.0" + "@jest/snapshot-utils" "30.5.1" + "@jest/transform" "30.5.1" + "@jest/types" "30.5.1" + babel-preset-current-node-syntax "^1.2.0" + chalk "^4.1.2" + expect "30.5.1" + graceful-fs "^4.2.11" + jest-diff "30.5.1" + jest-matcher-utils "30.5.1" + jest-message-util "30.5.1" + jest-util "30.5.1" + pretty-format "30.5.1" + semver "^7.7.2" + synckit "^0.11.8" + +jest-util@30.5.1: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-30.5.1.tgz#1e71a1ee24f365c34001c1f8aab6d6f52ceadcb7" + integrity sha512-yKuxmNy2rSbTXw+3SIPanJo+nV4/BS1p26v44IYBFMsswSQySfMMcPHErnOncda7i9HEz0q605rIhSTBVgrZTg== + dependencies: + "@jest/types" "30.5.1" + "@types/node" "*" + chalk "^4.1.2" + ci-info "^4.2.0" + graceful-fs "^4.2.11" + picomatch "^4.0.3" + +jest-validate@30.5.1: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-30.5.1.tgz#0922700fae123c9e1d8403bdf62924b7e3c668de" + integrity sha512-i/buJ56wTpxihE93hQYNMfdOThS87+HGvLZzZJmU4xggPcdTYQq051iwALLCHp3q+SkCGH+EFejQZz5EbBSkkg== + dependencies: + "@jest/get-type" "30.5.0" + "@jest/types" "30.5.1" + camelcase "^6.3.0" + chalk "^4.1.2" + leven "^3.1.0" + pretty-format "30.5.1" + +jest-watcher@30.5.1: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-30.5.1.tgz#70fc6bbcc26282b1bae5d53d7e46259e266b238d" + integrity sha512-+FHJ7C+S7b3ySfhA1aFmoa6TztsnwZVv84ycPRn0tVN93np2EU4b8C/KadqA3l6igdjgLBTnUotab9ER0HLG8A== + dependencies: + "@jest/test-result" "30.5.1" + "@jest/types" "30.5.1" + "@types/node" "*" + ansi-escapes "^4.3.2" + chalk "^4.1.2" + emittery "^0.13.1" + jest-util "30.5.1" + string-length "^4.0.2" + +jest-worker@30.5.1: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-30.5.1.tgz#32a4c17502addef713411ca3bf951796de83e2ba" + integrity sha512-Cbxh5v7AoLuFRmFJSM4/aHdQ68rjXvUWr716EE0Dh3I7T+T/3FgFKhOERGXHcU2Meftq9+9zxPM3TSNyI9D+HA== + dependencies: + "@types/node" "*" + "@ungap/structured-clone" "^1.3.0" + jest-util "30.5.1" + merge-stream "^2.0.0" + supports-color "^8.1.1" + +jest@^30.2.0: + version "30.5.1" + resolved "https://registry.yarnpkg.com/jest/-/jest-30.5.1.tgz#db781144fcff8b4859d8dd5a0108d4c3b2173cfd" + integrity sha512-3qrR8+ZXFnn7y0H2yjWQNkGGBLBY4zTRoTMAq9zJcgwLLtlyonfsCLviIXK9xuE2KgIyM+M36AFcoK2DgFR36w== + dependencies: + "@jest/core" "30.5.1" + "@jest/types" "30.5.1" + import-local "^3.2.0" + jest-cli "30.5.1" + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.15.2" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.15.2.tgz#3f83823ac6be17f570f23b2ecdef3777ff5ea364" + integrity sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsesc@^3.0.2, jsesc@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +jsonwebtoken@^9.0.0: + version "9.0.3" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz#6cd57ab01e9b0ac07cb847d53d3c9b6ee31f7ae2" + integrity sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g== + dependencies: + jws "^4.0.1" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + +jszip@^3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" + integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + setimmediate "^1.0.5" + +jwa@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804" + integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg== + dependencies: + buffer-equal-constant-time "^1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.1.tgz#07edc1be8fac20e677b283ece261498bd38f0690" + integrity sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA== + dependencies: + jwa "^2.0.1" + safe-buffer "^5.0.1" + +lazystream@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" + integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw== + dependencies: + readable-stream "^2.0.5" + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +listenercount@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" + integrity sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + +lodash.difference@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" + integrity sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA== + +lodash.escaperegexp@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" + integrity sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw== + +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g== + +lodash.groupby@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.groupby/-/lodash.groupby-4.6.0.tgz#0b08a1dcf68397c397855c3239783832df7403d1" + integrity sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw== + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + +lodash.isfunction@^3.0.9: + version "3.0.9" + resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051" + integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnil@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/lodash.isnil/-/lodash.isnil-4.0.0.tgz#49e28cd559013458c814c5479d3c663a21bfaa6c" + integrity sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + +lodash.isundefined@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz#23ef3d9535565203a66cefd5b830f848911afb48" + integrity sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA== + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + +lodash.union@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" + integrity sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw== + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== + +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +lru-cache@^11.0.0: + version "11.5.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.5.2.tgz#00e16665c90c620fba14a3c368732a976493f760" + integrity sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +make-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" + integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== + dependencies: + semver "^7.5.3" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +media-typer@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.1.tgz#6f035400dfe3ab9d5607bc77546ce30cc2f9c6b8" + integrity sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ== + +merge-descriptors@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808" + integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== + +merge-stream@^2.0.0: + version "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== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + +mime-types@^2.1.35: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime-types@^3.0.0, mime-types@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab" + integrity sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A== + 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" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@^10.2.1, minimatch@^10.2.2: + version "10.2.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.6.tgz#fd956bbe0b77241e9f15ac5dccb1c638060968ef" + integrity sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A== + dependencies: + brace-expansion "^5.0.8" + +minimatch@^3.1.1: + version "3.1.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e" + integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.1.0: + version "5.1.9" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.9.tgz#1293ef15db0098b394540e8f9f744f9fda8dee4b" + integrity sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.4: + version "9.0.9" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e" + integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg== + dependencies: + brace-expansion "^2.0.2" + +minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2, minipass@^7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.3.tgz#79389b4eb1bb2d003a9bba87d492f2bd37bdc65b" + integrity sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A== + +"mkdirp@>=0.5 0": + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +ms@^2.1.1, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +napi-postinstall@^0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/napi-postinstall/-/napi-postinstall-0.3.4.tgz#7af256d6588b5f8e952b9190965d6b019653bbb9" + integrity sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +negotiator@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.1.0.tgz#16e003d0db4ac24fd9df168edf871625deeae3df" + integrity sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg== + dependencies: + content-type "^2.1.0" + +node-addon-api@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" + integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +node-releases@^2.0.54: + version "2.0.54" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.54.tgz#09af17d5647aa9f221ec5cf2becb95b68a981afe" + integrity sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ== + +nodemon@^3.1.10: + version "3.1.14" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.14.tgz#8487ca379c515301d221ec007f27f24ecafa2b51" + integrity sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw== + dependencies: + chokidar "^3.5.2" + debug "^4" + ignore-by-default "^1.0.1" + minimatch "^10.2.1" + pstree.remy "^1.1.8" + semver "^7.5.3" + simple-update-notifier "^2.0.0" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.5" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +object-assign@^4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.3, object-inspect@^1.13.4: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + +on-finished@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.3.0, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + +pako@~1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + +parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parseurl@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-scurry@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.2.tgz#6be0d0ee02a10d9e0de7a98bae65e182c9061f85" + integrity sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg== + dependencies: + lru-cache "^11.0.0" + minipass "^7.1.2" + +path-to-regexp@^8.0.0: + version "8.4.2" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.4.2.tgz#795c420c4f7ca45c5b887366f622ee0c9852cccd" + integrity sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601" + integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA== + +picomatch@^4.0.3, picomatch@^4.0.4: + version "4.0.7" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.7.tgz#6313360034ccb36b3dc61ecbdff78121f90fe21f" + integrity sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA== + +pirates@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.7.tgz#643b4a18c4257c8a65104b73f3049ce9a0a15e22" + integrity sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pretty-format@30.5.1, pretty-format@^30.0.0: + version "30.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-30.5.1.tgz#0dda910a75d12346b771977b1f328517b9e846d4" + integrity sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg== + dependencies: + "@jest/react-is-18" "npm:react-is@^18.3.1" + "@jest/react-is-19" "npm:react-is@^19.2.5" + "@jest/schemas" "30.5.0" + ansi-styles "^5.2.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +proxy-addr@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + +pure-rand@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-7.0.1.tgz#6f53a5a9e3e4a47445822af96821ca509ed37566" + integrity sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ== + +qs@^6.14.0, qs@^6.14.1, qs@^6.15.2: + version "6.16.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.16.0.tgz#c22c723a28a920f3aacdce8289fabd43eccb79fd" + integrity sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA== + dependencies: + es-define-property "^1.0.1" + side-channel "^1.1.1" + +range-parser@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.3.0.tgz#d7f19be812bb62721472b45d3be219ef09572b47" + integrity sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw== + +raw-body@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.2.tgz#3e3ada5ae5568f9095d84376fd3a49b8fb000a51" + integrity sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA== + dependencies: + bytes "~3.1.2" + http-errors "~2.0.1" + iconv-lite "~0.7.0" + unpipe "~1.0.0" + +readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@~2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdir-glob@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.3.tgz#c3d831f51f5e7bfa62fa2ffbe4b508c640f09584" + integrity sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA== + dependencies: + minimatch "^5.1.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +redis@^5.9.0: + version "5.12.1" + resolved "https://registry.yarnpkg.com/redis/-/redis-5.12.1.tgz#f95297e01eca8b87a109601a2418647e05f16bee" + integrity sha512-LDsoVvb/CpoV9EN3FXvgvSHNJWuCIzl9MiO3ppOevuGLpSGJhwfQjpEwfFJcQvNSddHADDdZaWx0HnmMxRXG7g== + dependencies: + "@redis/bloom" "5.12.1" + "@redis/client" "5.12.1" + "@redis/json" "5.12.1" + "@redis/search" "5.12.1" + "@redis/time-series" "5.12.1" + +regenerate-unicode-properties@^10.2.2: + version "10.2.2" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz#aa113812ba899b630658c7623466be71e1f86f66" + integrity sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regexpu-core@^6.3.1: + version "6.4.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.4.0.tgz#3580ce0c4faedef599eccb146612436b62a176e5" + integrity sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.2.2" + regjsgen "^0.8.0" + regjsparser "^0.13.0" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.2.1" + +regjsgen@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" + integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== + +regjsparser@^0.13.0: + version "0.13.2" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.13.2.tgz#f654734b5c588b22ba3e21693b30523417180808" + integrity sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ== + dependencies: + jsesc "~3.1.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.22.11: + version "1.22.12" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.12.tgz#f5b2a680897c69c238a13cd16b15671f8b73549f" + integrity sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA== + dependencies: + es-errors "^1.3.0" + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rimraf@2: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +router@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef" + integrity sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ== + dependencies: + debug "^4.4.0" + depd "^2.0.0" + is-promise "^4.0.0" + parseurl "^1.3.3" + path-to-regexp "^8.0.0" + +safe-buffer@^5.0.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +saxes@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" + integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== + dependencies: + xmlchars "^2.2.0" + +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.5.3, semver@^7.5.4, semver@^7.7.2: + version "7.8.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.5.tgz#39b646037dd50c14fb451e7e4cac58ed8b863f69" + integrity sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA== + +send@^1.1.0, send@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/send/-/send-1.2.1.tgz#9eab743b874f3550f40a26867bf286ad60d3f3ed" + integrity sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ== + 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" + +serve-static@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.2.1.tgz#7f186a4a4e5f5b663ad7a4294ff1bf37cf0e98a9" + integrity sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw== + dependencies: + encodeurl "^2.0.0" + escape-html "^1.0.3" + parseurl "^1.3.3" + send "^1.2.0" + +setimmediate@^1.0.5, setimmediate@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + +setprototypeof@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel-list@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.1.tgz#c2e0b5a14a540aebee3bbc6c3f8666cc9b509127" + integrity sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.4" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + 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" + +side-channel@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.1.tgz#ea02c62e05dc4bea67d4442f0fb71ee192f8e0ab" + integrity sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.4" + side-channel-list "^1.0.1" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + +signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +simple-json-db@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/simple-json-db/-/simple-json-db-2.0.0.tgz#5ed27b0ed16f909506a72953f17eda49e278601d" + integrity sha512-oTh7gFQzqAe0E8RN3EkisPo0CojkzcKCKibTcJncg0yt47hWTaNwwjX/FsxfXSTDxfMjBFXFVnZe/EskAlJr7w== + +simple-update-notifier@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb" + integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w== + dependencies: + semver "^7.5.3" + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +stack-utils@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + +statuses@^2.0.1, statuses@^2.0.2, statuses@~2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== + +string-length@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.2.0.tgz#d22a269522836a627af8d04b5c3fd2c7fa3e32e3" + integrity sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w== + dependencies: + ansi-regex "^6.2.2" + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@^3.1.1: + version "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" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +synckit@^0.11.8: + version "0.11.13" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.13.tgz#062a5ea57d81befc35892f8254de5c567e97c80a" + integrity sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg== + dependencies: + "@pkgr/core" "^0.3.6" + +tar-stream@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +test-exclude@^7.0.1: + version "7.0.2" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-7.0.2.tgz#482392077630bc57d5630c13abe908bb910dfc65" + integrity sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^10.4.1" + minimatch "^10.2.2" + +tmp@^0.2.0: + version "0.2.7" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.7.tgz#26f4db11d1601ce8012dcb8a798ece1c06a99059" + integrity sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +touch@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694" + integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA== + +"traverse@>=0.3.0 <0.4": + version "0.3.9" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" + integrity sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ== + +ts-node@^10.9.1: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +tslib@^2.4.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-is@^2.0.1, type-is@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.1.0.tgz#71d1a7053293582e16ac9f3ebaf1ab9aa49e5570" + integrity sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA== + dependencies: + content-type "^2.0.0" + media-typer "^1.1.0" + mime-types "^3.0.0" + +typescript@^5.9.3: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + +undici-types@~7.18.0: + version "7.18.2" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.18.2.tgz#29357a89e7b7ca4aef3bf0fd3fd0cd73884229e9" + integrity sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w== + +undici-types@~8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-8.3.0.tgz#44e9fc9f3244648cdea35e4f9bb2d681e9410809" + integrity sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ== + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2" + integrity sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz#65a7adfad8574c219890e219285ce4c64ed67eaa" + integrity sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz#301d4f8a43d2b75c97adfad87c9dd5350c9475d1" + integrity sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ== + +unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +unrs-resolver@^1.12.1: + version "1.12.2" + resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.12.2.tgz#a6c6888396abba5adaac4cab6587df866f1d7afd" + integrity sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ== + dependencies: + napi-postinstall "^0.3.4" + optionalDependencies: + "@unrs/resolver-binding-android-arm-eabi" "1.12.2" + "@unrs/resolver-binding-android-arm64" "1.12.2" + "@unrs/resolver-binding-darwin-arm64" "1.12.2" + "@unrs/resolver-binding-darwin-x64" "1.12.2" + "@unrs/resolver-binding-freebsd-x64" "1.12.2" + "@unrs/resolver-binding-linux-arm-gnueabihf" "1.12.2" + "@unrs/resolver-binding-linux-arm-musleabihf" "1.12.2" + "@unrs/resolver-binding-linux-arm64-gnu" "1.12.2" + "@unrs/resolver-binding-linux-arm64-musl" "1.12.2" + "@unrs/resolver-binding-linux-loong64-gnu" "1.12.2" + "@unrs/resolver-binding-linux-loong64-musl" "1.12.2" + "@unrs/resolver-binding-linux-ppc64-gnu" "1.12.2" + "@unrs/resolver-binding-linux-riscv64-gnu" "1.12.2" + "@unrs/resolver-binding-linux-riscv64-musl" "1.12.2" + "@unrs/resolver-binding-linux-s390x-gnu" "1.12.2" + "@unrs/resolver-binding-linux-x64-gnu" "1.12.2" + "@unrs/resolver-binding-linux-x64-musl" "1.12.2" + "@unrs/resolver-binding-openharmony-arm64" "1.12.2" + "@unrs/resolver-binding-wasm32-wasi" "1.12.2" + "@unrs/resolver-binding-win32-arm64-msvc" "1.12.2" + "@unrs/resolver-binding-win32-ia32-msvc" "1.12.2" + "@unrs/resolver-binding-win32-x64-msvc" "1.12.2" + +unzipper@^0.10.11: + version "0.10.14" + resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.10.14.tgz#d2b33c977714da0fbc0f82774ad35470a7c962b1" + integrity sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g== + dependencies: + big-integer "^1.6.17" + binary "~0.3.0" + bluebird "~3.4.1" + buffer-indexof-polyfill "~1.0.0" + duplexer2 "~0.1.4" + fstream "^1.0.12" + graceful-fs "^4.2.2" + listenercount "~1.0.1" + readable-stream "~2.3.6" + setimmediate "~1.0.4" + +update-browserslist-db@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz#9d99fbff56c50bb11ba5fd35cece5916da595836" + integrity sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +uuid@^8.3.0: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +v8-to-istanbul@^9.0.1: + version "9.3.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175" + integrity sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.12" + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^2.0.0" + +vary@^1, vary@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-5.0.1.tgz#68df4717c55c6fa4281a7860b4c2ba0a6d2b11e7" + integrity sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^4.0.1" + +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.7.2: + version "17.7.3" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.3.tgz#779dffe6bcafec596a7172e983289a588647faaa" + integrity sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g== + 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" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zip-stream@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.1.1.tgz#1337fe974dbaffd2fa9a1ba09662a66932bd7135" + integrity sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ== + dependencies: + archiver-utils "^3.0.4" + compress-commons "^4.1.2" + readable-stream "^3.6.0" diff --git a/types/api.yml b/types/api.yml new file mode 100644 index 0000000..08cbf99 --- /dev/null +++ b/types/api.yml @@ -0,0 +1,75 @@ +openapi: 3.0.4 +info: + title: Food Tracer API + version: 1.0.0 +servers: + - url: /api +paths: + # Obecné (/api) + /login: + $ref: "./paths/login.yml" + /config: + $ref: "./paths/config/config.yml" + + # Jídla (/api/meals) + /meals/day: + $ref: "./paths/meals/day.yml" + /meals/range: + $ref: "./paths/meals/range.yml" + /meals/add: + $ref: "./paths/meals/add.yml" + /meals/update: + $ref: "./paths/meals/update.yml" + /meals/delete: + $ref: "./paths/meals/delete.yml" + + # Statistiky (/api/stats) + /stats/summary: + $ref: "./paths/stats/summary.yml" + + # Přehled dne (/api/day) + /day: + $ref: "./paths/day/overview.yml" + + # Pohyb (/api/activities) + /activities/add: + $ref: "./paths/activities/add.yml" + /activities/update: + $ref: "./paths/activities/update.yml" + /activities/delete: + $ref: "./paths/activities/delete.yml" + + # Šablony tréninků (/api/workouts) + /workouts: + $ref: "./paths/workouts/list.yml" + /workouts/save: + $ref: "./paths/workouts/save.yml" + /workouts/delete: + $ref: "./paths/workouts/delete.yml" + /workouts/apply: + $ref: "./paths/workouts/apply.yml" + + # Kalorie (/api/calories) + /calories/search: + $ref: "./paths/calories/search.yml" + + # Nastavení (/api/settings) + /settings: + $ref: "./paths/settings/settings.yml" + /settings/basal: + $ref: "./paths/settings/basal.yml" + + # Importy (/api/import) + /import/luncher: + $ref: "./paths/import/luncher.yml" + +components: + schemas: + $ref: "./schemas/_index.yml" + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT +security: + - bearerAuth: [] diff --git a/types/index.ts b/types/index.ts new file mode 100644 index 0000000..8dfed38 --- /dev/null +++ b/types/index.ts @@ -0,0 +1 @@ +export * from './gen'; diff --git a/types/openapi-ts.config.ts b/types/openapi-ts.config.ts new file mode 100644 index 0000000..4be9999 --- /dev/null +++ b/types/openapi-ts.config.ts @@ -0,0 +1,14 @@ +import { defaultPlugins } from '@hey-api/openapi-ts'; + +export default { + input: 'api.yml', + output: 'gen', + plugins: [ + ...defaultPlugins, + '@hey-api/client-fetch', + { + enums: 'javascript', + name: '@hey-api/typescript', + }, + ], +}; diff --git a/types/package.json b/types/package.json new file mode 100644 index 0000000..ffda851 --- /dev/null +++ b/types/package.json @@ -0,0 +1,14 @@ +{ + "name": "@food-tracer/types", + "version": "1.0.0", + "license": "MIT", + "private": true, + "scripts": { + "openapi-ts": "openapi-ts" + }, + "devDependencies": { + "@hey-api/client-fetch": "^0.8.2", + "@hey-api/openapi-ts": "^0.64.7", + "typescript": "^5.9.3" + } +} diff --git a/types/paths/activities/add.yml b/types/paths/activities/add.yml new file mode 100644 index 0000000..bf94ad0 --- /dev/null +++ b/types/paths/activities/add.yml @@ -0,0 +1,20 @@ +post: + operationId: addActivity + summary: Přidá pohybovou aktivitu do zvoleného dne. + requestBody: + required: true + content: + application/json: + schema: + $ref: "../../schemas/_index.yml#/ActivityInput" + responses: + "200": + description: Aktualizovaný přehled dne + content: + application/json: + schema: + $ref: "../../schemas/_index.yml#/DayOverview" + "400": + description: Neplatná data + "401": + description: Neautentizovaný uživatel diff --git a/types/paths/activities/delete.yml b/types/paths/activities/delete.yml new file mode 100644 index 0000000..a533600 --- /dev/null +++ b/types/paths/activities/delete.yml @@ -0,0 +1,29 @@ +post: + operationId: deleteActivity + summary: Smaže aktivitu. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [id, date] + properties: + id: + type: string + date: + type: string + format: date + responses: + "200": + description: Aktualizovaný přehled dne + content: + application/json: + schema: + $ref: "../../schemas/_index.yml#/DayOverview" + "400": + description: Neplatná data + "401": + description: Neautentizovaný uživatel + "404": + description: Záznam neexistuje diff --git a/types/paths/activities/update.yml b/types/paths/activities/update.yml new file mode 100644 index 0000000..efe87a0 --- /dev/null +++ b/types/paths/activities/update.yml @@ -0,0 +1,28 @@ +post: + operationId: updateActivity + summary: Upraví existující aktivitu. + requestBody: + required: true + content: + application/json: + schema: + allOf: + - type: object + required: [id] + properties: + id: + type: string + - $ref: "../../schemas/_index.yml#/ActivityInput" + responses: + "200": + description: Aktualizovaný přehled dne + content: + application/json: + schema: + $ref: "../../schemas/_index.yml#/DayOverview" + "400": + description: Neplatná data + "401": + description: Neautentizovaný uživatel + "404": + description: Záznam neexistuje diff --git a/types/paths/calories/search.yml b/types/paths/calories/search.yml new file mode 100644 index 0000000..8c1af49 --- /dev/null +++ b/types/paths/calories/search.yml @@ -0,0 +1,23 @@ +get: + operationId: searchCalories + summary: | + Najde návrhy energetické hodnoty pro název jídla — z vlastní knihovny + dřívějších zadání a od externího poskytovatele. + parameters: + - in: query + name: q + required: true + schema: + type: string + description: Název jídla, ke kterému se hledá energetická hodnota + responses: + "200": + description: Nalezené návrhy + content: + application/json: + schema: + $ref: "../../schemas/_index.yml#/CalorieSearchResult" + "400": + description: Nebyl předán hledaný název + "401": + description: Neautentizovaný uživatel diff --git a/types/paths/config/config.yml b/types/paths/config/config.yml new file mode 100644 index 0000000..c40c6cd --- /dev/null +++ b/types/paths/config/config.yml @@ -0,0 +1,11 @@ +get: + operationId: getConfig + summary: Vrátí veřejnou runtime konfiguraci klienta. + security: [] + responses: + "200": + description: Runtime konfigurace + content: + application/json: + schema: + $ref: "../../schemas/_index.yml#/Config" diff --git a/types/paths/day/overview.yml b/types/paths/day/overview.yml new file mode 100644 index 0000000..9a211da --- /dev/null +++ b/types/paths/day/overview.yml @@ -0,0 +1,22 @@ +get: + operationId: getDayOverview + summary: Vrátí kompletní přehled dne — jídlo, pohyb a energetickou bilanci. + parameters: + - in: query + name: date + required: false + schema: + type: string + format: date + description: Datum ve formátu YYYY-MM-DD. Výchozí je dnešek. + responses: + "200": + description: Přehled dne + content: + application/json: + schema: + $ref: "../../schemas/_index.yml#/DayOverview" + "400": + description: Neplatné datum + "401": + description: Neautentizovaný uživatel diff --git a/types/paths/import/luncher.yml b/types/paths/import/luncher.yml new file mode 100644 index 0000000..1537b68 --- /dev/null +++ b/types/paths/import/luncher.yml @@ -0,0 +1,22 @@ +post: + operationId: importLuncher + summary: | + Naimportuje měsíční přehled vyexportovaný z Luncheru (XLSX, CSV nebo JSON). + Řádky se založí do dnů, ke kterým patří, ve výchozím stavu jako oběd. + requestBody: + required: true + content: + application/json: + schema: + $ref: "../../schemas/_index.yml#/ImportRequest" + responses: + "200": + description: Výsledek importu, nebo jeho náhled při dryRun + content: + application/json: + schema: + $ref: "../../schemas/_index.yml#/ImportResult" + "400": + description: Nepodporovaný formát nebo poškozený soubor + "401": + description: Neautentizovaný uživatel diff --git a/types/paths/login.yml b/types/paths/login.yml new file mode 100644 index 0000000..1178fdf --- /dev/null +++ b/types/paths/login.yml @@ -0,0 +1,22 @@ +post: + operationId: login + summary: Přihlásí uživatele a vrátí JWT token. + security: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + login: + type: string + responses: + "200": + description: JWT token přihlášeného uživatele + content: + application/json: + schema: + type: string + "500": + description: Nebyl předán login diff --git a/types/paths/meals/add.yml b/types/paths/meals/add.yml new file mode 100644 index 0000000..4b2f194 --- /dev/null +++ b/types/paths/meals/add.yml @@ -0,0 +1,20 @@ +post: + operationId: addMeal + summary: Přidá jídlo do zvoleného dne. + requestBody: + required: true + content: + application/json: + schema: + $ref: "../../schemas/_index.yml#/MealInput" + responses: + "200": + description: Aktualizovaný přehled dne + content: + application/json: + schema: + $ref: "../../schemas/_index.yml#/DayRecord" + "400": + description: Neplatná data + "401": + description: Neautentizovaný uživatel diff --git a/types/paths/meals/day.yml b/types/paths/meals/day.yml new file mode 100644 index 0000000..170a569 --- /dev/null +++ b/types/paths/meals/day.yml @@ -0,0 +1,22 @@ +get: + operationId: getDay + summary: Vrátí jídla přihlášeného uživatele za jeden den. + parameters: + - in: query + name: date + required: false + schema: + type: string + format: date + description: Datum ve formátu YYYY-MM-DD. Výchozí je dnešek. + responses: + "200": + description: Přehled jídel daného dne + content: + application/json: + schema: + $ref: "../../schemas/_index.yml#/DayRecord" + "400": + description: Neplatné datum + "401": + description: Neautentizovaný uživatel diff --git a/types/paths/meals/delete.yml b/types/paths/meals/delete.yml new file mode 100644 index 0000000..a029543 --- /dev/null +++ b/types/paths/meals/delete.yml @@ -0,0 +1,31 @@ +post: + operationId: deleteMeal + summary: Smaže záznam o jídle. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [id, date] + properties: + id: + description: Identifikátor mazaného záznamu + type: string + date: + description: Datum záznamu (YYYY-MM-DD) + type: string + format: date + responses: + "200": + description: Aktualizovaný přehled dne + content: + application/json: + schema: + $ref: "../../schemas/_index.yml#/DayRecord" + "400": + description: Neplatná data + "401": + description: Neautentizovaný uživatel + "404": + description: Záznam neexistuje diff --git a/types/paths/meals/range.yml b/types/paths/meals/range.yml new file mode 100644 index 0000000..b2a067c --- /dev/null +++ b/types/paths/meals/range.yml @@ -0,0 +1,31 @@ +get: + operationId: getRange + summary: Vrátí jídla přihlášeného uživatele za rozsah dnů. + parameters: + - in: query + name: from + required: true + schema: + type: string + format: date + description: První den rozsahu (včetně), YYYY-MM-DD + - in: query + name: to + required: true + schema: + type: string + format: date + description: Poslední den rozsahu (včetně), YYYY-MM-DD + responses: + "200": + description: Dny s alespoň jedním záznamem, vzestupně dle data + content: + application/json: + schema: + type: array + items: + $ref: "../../schemas/_index.yml#/DayRecord" + "400": + description: Neplatný rozsah + "401": + description: Neautentizovaný uživatel diff --git a/types/paths/meals/update.yml b/types/paths/meals/update.yml new file mode 100644 index 0000000..e03020c --- /dev/null +++ b/types/paths/meals/update.yml @@ -0,0 +1,29 @@ +post: + operationId: updateMeal + summary: Upraví existující záznam o jídle. + requestBody: + required: true + content: + application/json: + schema: + allOf: + - type: object + required: [id] + properties: + id: + description: Identifikátor upravovaného záznamu + type: string + - $ref: "../../schemas/_index.yml#/MealInput" + responses: + "200": + description: Aktualizovaný přehled dne + content: + application/json: + schema: + $ref: "../../schemas/_index.yml#/DayRecord" + "400": + description: Neplatná data + "401": + description: Neautentizovaný uživatel + "404": + description: Záznam neexistuje diff --git a/types/paths/settings/basal.yml b/types/paths/settings/basal.yml new file mode 100644 index 0000000..c299b91 --- /dev/null +++ b/types/paths/settings/basal.yml @@ -0,0 +1,25 @@ +post: + operationId: saveBasalCalories + summary: Uloží klidový výdej (bazální metabolismus) přihlášeného uživatele. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + basalCalories: + description: Klidový výdej v kcal za den. Null nebo 0 ho odstraní. + type: integer + nullable: true + responses: + "200": + description: Aktualizované nastavení + content: + application/json: + schema: + $ref: "../../schemas/_index.yml#/UserSettings" + "400": + description: Neplatná data + "401": + description: Neautentizovaný uživatel diff --git a/types/paths/settings/settings.yml b/types/paths/settings/settings.yml new file mode 100644 index 0000000..a912b03 --- /dev/null +++ b/types/paths/settings/settings.yml @@ -0,0 +1,41 @@ +get: + operationId: getSettings + summary: Vrátí nastavení přihlášeného uživatele. + responses: + "200": + description: Nastavení uživatele + content: + application/json: + schema: + $ref: "../../schemas/_index.yml#/UserSettings" + "401": + description: Neautentizovaný uživatel +post: + operationId: saveSourceRate + summary: Uloží nebo smaže cenu za 100 g u jednoho zdroje. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [source] + properties: + source: + description: Název zdroje (podniku) + type: string + pricePer100g: + description: Cena za 100 g v haléřích. Null nebo 0 sazbu odstraní. + type: integer + nullable: true + responses: + "200": + description: Aktualizované nastavení + content: + application/json: + schema: + $ref: "../../schemas/_index.yml#/UserSettings" + "400": + description: Neplatná data + "401": + description: Neautentizovaný uživatel diff --git a/types/paths/stats/summary.yml b/types/paths/stats/summary.yml new file mode 100644 index 0000000..79d20f6 --- /dev/null +++ b/types/paths/stats/summary.yml @@ -0,0 +1,29 @@ +get: + operationId: getFoodStats + summary: Vrátí statistiky útraty a jídel za zvolené období. + parameters: + - in: query + name: from + required: true + schema: + type: string + format: date + description: První den období (včetně), YYYY-MM-DD + - in: query + name: to + required: true + schema: + type: string + format: date + description: Poslední den období (včetně), YYYY-MM-DD + responses: + "200": + description: Statistiky za období + content: + application/json: + schema: + $ref: "../../schemas/_index.yml#/FoodStats" + "400": + description: Neplatný rozsah + "401": + description: Neautentizovaný uživatel diff --git a/types/paths/workouts/apply.yml b/types/paths/workouts/apply.yml new file mode 100644 index 0000000..c7be797 --- /dev/null +++ b/types/paths/workouts/apply.yml @@ -0,0 +1,31 @@ +post: + operationId: applyWorkoutTemplate + summary: Založí do zvoleného dne všechny položky šablony tréninku. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [id, date] + properties: + id: + description: Identifikátor šablony + type: string + date: + description: Den, do kterého se položky založí + type: string + format: date + responses: + "200": + description: Aktualizovaný přehled dne + content: + application/json: + schema: + $ref: "../../schemas/_index.yml#/DayOverview" + "400": + description: Neplatná data + "401": + description: Neautentizovaný uživatel + "404": + description: Šablona neexistuje diff --git a/types/paths/workouts/delete.yml b/types/paths/workouts/delete.yml new file mode 100644 index 0000000..171e474 --- /dev/null +++ b/types/paths/workouts/delete.yml @@ -0,0 +1,26 @@ +post: + operationId: deleteWorkoutTemplate + summary: Smaže šablonu tréninku. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [id] + properties: + id: + type: string + responses: + "200": + description: Aktualizovaný seznam šablon + content: + application/json: + schema: + type: array + items: + $ref: "../../schemas/_index.yml#/WorkoutTemplate" + "400": + description: Neplatná data + "401": + description: Neautentizovaný uživatel diff --git a/types/paths/workouts/list.yml b/types/paths/workouts/list.yml new file mode 100644 index 0000000..ecdf324 --- /dev/null +++ b/types/paths/workouts/list.yml @@ -0,0 +1,14 @@ +get: + operationId: getWorkoutTemplates + summary: Vrátí šablony tréninků přihlášeného uživatele. + responses: + "200": + description: Šablony seřazené podle názvu + content: + application/json: + schema: + type: array + items: + $ref: "../../schemas/_index.yml#/WorkoutTemplate" + "401": + description: Neautentizovaný uživatel diff --git a/types/paths/workouts/save.yml b/types/paths/workouts/save.yml new file mode 100644 index 0000000..8bfea62 --- /dev/null +++ b/types/paths/workouts/save.yml @@ -0,0 +1,33 @@ +post: + operationId: saveWorkoutTemplate + summary: Založí novou šablonu tréninku, nebo upraví existující. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [name, items] + properties: + id: + description: Identifikátor upravované šablony. Bez něj se založí nová. + type: string + name: + type: string + items: + type: array + items: + $ref: "../../schemas/_index.yml#/WorkoutTemplateItem" + responses: + "200": + description: Aktualizovaný seznam šablon + content: + application/json: + schema: + type: array + items: + $ref: "../../schemas/_index.yml#/WorkoutTemplate" + "400": + description: Neplatná data + "401": + description: Neautentizovaný uživatel diff --git a/types/schemas/_index.yml b/types/schemas/_index.yml new file mode 100644 index 0000000..2489f43 --- /dev/null +++ b/types/schemas/_index.yml @@ -0,0 +1,664 @@ +MealType: + description: | + Typ jídla (chod v rámci dne). Importy z Luncheru se zakládají jako OBED, + pokud se při importu neurčí jinak. + type: string + enum: + - SNIDANE + - DOPOLEDNI_SVACINA + - OBED + - ODPOLEDNI_SVACINA + - VECERE + - JINE + +MealEntry: + description: Jeden snědený pokrm v konkrétní den + type: object + additionalProperties: false + required: + - id + - date + - mealType + - name + - createdAt + properties: + id: + description: Identifikátor záznamu (UUID) + type: string + date: + description: Datum konzumace ve formátu YYYY-MM-DD + type: string + format: date + mealType: + $ref: "#/MealType" + name: + description: Název jídla + type: string + source: + description: Zdroj jídla (podnik, obchod, "doma", ...) + type: string + price: + description: Cena v haléřích. Celé číslo, aby nevznikaly chyby zaokrouhlením. + type: integer + calories: + description: | + Energetická hodnota v kcal. Pokud je vyplněná gramáž i caloriesPer100g, + dopočte se z nich; jinak platí ručně zadaná hodnota. + type: integer + weight: + description: | + Gramáž porce v gramech. U podniků, které účtují podle váhy (TechTower), + se dopočítá z ceny a pricePer100g, ale dá se přepsat. + type: integer + pricePer100g: + description: Cena za 100 g v haléřích. Slouží k dopočtu gramáže z ceny. + type: integer + caloriesPer100g: + description: Energetická hodnota na 100 g v kcal. Slouží k dopočtu kalorií z gramáže. + type: integer + note: + description: Poznámka uživatele k jídlu + type: string + importSource: + description: Odkud byl záznam naimportován (např. "luncher"). U ručně přidaných chybí. + type: string + importKey: + description: | + Klíč pro rozpoznání duplicit při opakovaném importu stejného období. + Odvozuje se z data, typu, názvu jídla a částky importovaného řádku. + type: string + createdAt: + description: Čas vzniku záznamu + type: string + format: date-time + updatedAt: + description: Čas poslední úpravy záznamu + type: string + format: date-time + +MealInput: + description: Data pro založení nebo úpravu záznamu o jídle + type: object + additionalProperties: false + required: + - date + - mealType + - name + properties: + date: + description: Datum konzumace ve formátu YYYY-MM-DD + type: string + format: date + mealType: + $ref: "#/MealType" + name: + description: Název jídla + type: string + source: + description: Zdroj jídla (podnik, obchod, "doma", ...) + type: string + price: + description: Cena v haléřích + type: integer + nullable: true + calories: + description: | + Energetická hodnota v kcal. Ignoruje se, pokud jde dopočíst + z gramáže a caloriesPer100g. + type: integer + nullable: true + weight: + description: Gramáž porce v gramech. Nevyplněná se dopočte z ceny a pricePer100g. + type: integer + nullable: true + pricePer100g: + description: Cena za 100 g v haléřích + type: integer + nullable: true + caloriesPer100g: + description: Energetická hodnota na 100 g v kcal + type: integer + nullable: true + note: + description: Poznámka uživatele k jídlu + type: string + nullable: true + +DayRecord: + description: Přehled jídel jednoho dne včetně součtů + type: object + additionalProperties: false + required: + - date + - entries + - totalPrice + - totalCalories + properties: + date: + description: Datum ve formátu YYYY-MM-DD + type: string + format: date + entries: + description: Jídla daného dne seřazená dle typu (snídaně → večeře) + type: array + items: + $ref: "#/MealEntry" + totalPrice: + description: Součet cen všech jídel dne v haléřích + type: integer + totalCalories: + description: Součet kalorií jídel dne v kcal (jídla bez kalorií se nezapočítávají) + type: integer + +PeriodTotal: + description: Součty za jedno období (den, měsíc nebo rok) + type: object + additionalProperties: false + required: + - period + - price + - calories + - count + properties: + period: + description: Označení období — YYYY-MM-DD (den), YYYY-MM (měsíc) nebo YYYY (rok) + type: string + price: + description: Utracená částka v haléřích + type: integer + calories: + description: Součet kalorií v kcal + type: integer + count: + description: Počet záznamů o jídle + type: integer + +GroupTotal: + description: Součty za jednu skupinu (typ jídla nebo zdroj) + type: object + additionalProperties: false + required: + - key + - price + - calories + - count + properties: + key: + description: Klíč skupiny (hodnota MealType, nebo název zdroje) + type: string + price: + description: Utracená částka v haléřích + type: integer + calories: + description: Součet kalorií v kcal + type: integer + count: + description: Počet záznamů o jídle + type: integer + +FoodStats: + description: Statistiky útraty a jídel za zvolené období + type: object + additionalProperties: false + required: + - from + - to + - totalPrice + - totalCalories + - entryCount + - dayCount + - byDay + - byMonth + - byYear + - byMealType + - bySource + properties: + from: + description: Začátek období (YYYY-MM-DD, včetně) + type: string + format: date + to: + description: Konec období (YYYY-MM-DD, včetně) + type: string + format: date + totalPrice: + description: Celková útrata za období v haléřích + type: integer + totalCalories: + description: Celkový součet kalorií za období v kcal + type: integer + entryCount: + description: Počet záznamů o jídle v období + type: integer + dayCount: + description: Počet dní, ve kterých je alespoň jeden záznam + type: integer + byDay: + description: Součty po dnech, vzestupně dle data + type: array + items: + $ref: "#/PeriodTotal" + byMonth: + description: Součty po měsících, vzestupně + type: array + items: + $ref: "#/PeriodTotal" + byYear: + description: Součty po letech, vzestupně + type: array + items: + $ref: "#/PeriodTotal" + byMealType: + description: Součty dle typu jídla, sestupně dle útraty + type: array + items: + $ref: "#/GroupTotal" + bySource: + description: Součty dle zdroje jídla, sestupně dle útraty + type: array + items: + $ref: "#/GroupTotal" + +ImportRequest: + description: Požadavek na import přehledu z Luncheru + type: object + additionalProperties: false + required: + - fileName + - content + properties: + fileName: + description: Původní název souboru — určuje formát (.xlsx, .csv, .json) + type: string + content: + description: Obsah souboru zakódovaný v Base64 + type: string + defaultMealType: + description: | + Typ jídla, pod kterým se importované řádky založí. + Luncher řeší výběr obědů, takže výchozí hodnota je OBED. + allOf: + - $ref: "#/MealType" + dryRun: + description: | + Pokud je true, import se pouze vyhodnotí a vrátí náhled, ale nic se neuloží. + type: boolean + +ImportResult: + description: Výsledek (nebo náhled) importu přehledu z Luncheru + type: object + additionalProperties: false + required: + - dryRun + - format + - rowCount + - imported + - skipped + - days + - totalPrice + - entries + - warnings + properties: + dryRun: + description: True, pokud šlo pouze o náhled a data se neuložila + type: boolean + format: + description: Rozpoznaný formát vstupního souboru + type: string + enum: [xlsx, csv, json] + rowCount: + description: Počet řádků nalezených v souboru + type: integer + imported: + description: Počet záznamů, které import založil (u náhledu by založil) + type: integer + skipped: + description: Počet přeskočených řádků, protože už z dřívějšího importu existují + type: integer + days: + description: Dny (YYYY-MM-DD), kterých se import dotkl, vzestupně + type: array + items: + type: string + format: date + totalPrice: + description: Součet cen importovaných záznamů v haléřích + type: integer + entries: + description: Záznamy, které import založil (u náhledu založí) + type: array + items: + $ref: "#/MealEntry" + warnings: + description: Upozornění na řádky, které se nepodařilo zpracovat + type: array + items: + type: string + +Config: + description: Veřejná runtime konfigurace pro klienta + type: object + additionalProperties: false + required: + - sentry + properties: + sentry: + type: object + additionalProperties: false + required: + - dsn + - environment + properties: + dsn: + type: string + nullable: true + environment: + type: string + +CalorieSuggestion: + description: Návrh energetické hodnoty pro jídlo + type: object + additionalProperties: false + required: + - name + - caloriesPer100g + - origin + properties: + name: + description: Název nalezené potraviny + type: string + caloriesPer100g: + description: Energetická hodnota na 100 g v kcal + type: integer + origin: + description: | + Odkud návrh pochází — "library" je dřívější vlastní zadání uživatele, + ostatní hodnoty označují externího poskytovatele. + type: string + brand: + description: Značka nebo výrobce, pokud je známý + type: string + +CalorieSearchResult: + description: Výsledek hledání energetické hodnoty pro název jídla + type: object + additionalProperties: false + required: + - query + - providerAvailable + - suggestions + - externalSearchUrl + properties: + query: + description: Hledaný název + type: string + providerAvailable: + description: | + False, pokud je externí poskytovatel nedostupný. Návrhy z vlastní + knihovny se vrací i tak — hledání kvůli tomu neselže. + type: boolean + providerName: + description: Název externího poskytovatele, který návrhy dodal + type: string + suggestions: + description: Nalezené návrhy, nejdřív z vlastní knihovny + type: array + items: + $ref: "#/CalorieSuggestion" + externalSearchUrl: + description: | + Odkaz na tabulku potravin na KalorickéTabulky.cz. Otevírá se uživateli + v novém panelu — hodnoty se odtud nestahují automaticky. Předvyplnit + hledání nejde, jejich vyhledávání běží v JavaScriptu a parametry v URL + ignoruje, proto klient název jídla kopíruje do schránky. + type: string + +SourceRate: + description: Cena za 100 g u jednoho zdroje jídla + type: object + additionalProperties: false + required: + - source + - pricePer100g + properties: + source: + description: Název zdroje (podniku), např. "TechTower" + type: string + pricePer100g: + description: Cena za 100 g v haléřích + type: integer + +UserSettings: + description: Uživatelské nastavení aplikace + type: object + additionalProperties: false + required: + - sourceRates + properties: + sourceRates: + description: | + Ceny za 100 g u podniků, které účtují podle váhy. Slouží jako výchozí + hodnota při zadávání jídla; u konkrétního jídla jde sazbu přepsat. + type: array + items: + $ref: "#/SourceRate" + basalCalories: + description: | + Klidový výdej (bazální metabolismus) v kcal za den. Bez něj se bilance dne + počítá jen jako jídlo proti pohybu a nejde o skutečný deficit. + type: integer + +ActivityUnit: + description: Jednotka, ve které se aktivita měří + type: string + enum: + - KROKY + - MINUTY + - KM + - OPAKOVANI + +ActivityEntry: + description: Jedna pohybová aktivita v konkrétní den + type: object + additionalProperties: false + required: + - id + - date + - name + - unit + - quantity + - createdAt + properties: + id: + description: Identifikátor záznamu (UUID) + type: string + date: + description: Datum aktivity ve formátu YYYY-MM-DD + type: string + format: date + name: + description: Název aktivity (Chůze, Kliky, Běh, ...) + type: string + unit: + $ref: "#/ActivityUnit" + quantity: + description: Množství v dané jednotce — počet kroků, minut, kilometrů nebo opakování + type: integer + caloriesPer100Units: + description: | + Spálené kcal na 100 jednotek. Pro kroky vychází kolem 4 (tj. 400 kcal + za 10 000 kroků), u minut běhu kolem 1000. Sto jednotek se používá kvůli + celým číslům — stejná konvence jako u energie jídla na 100 g. + type: integer + calories: + description: | + Spálené kcal. Pokud je vyplněné caloriesPer100Units, dopočte se z množství; + jinak platí ručně zadaná hodnota. + type: integer + note: + description: Poznámka uživatele k aktivitě + type: string + templateId: + description: Identifikátor šablony, ze které aktivita vznikla + type: string + createdAt: + description: Čas vzniku záznamu + type: string + format: date-time + updatedAt: + description: Čas poslední úpravy záznamu + type: string + format: date-time + +ActivityInput: + description: Data pro založení nebo úpravu aktivity + type: object + additionalProperties: false + required: + - date + - name + - unit + - quantity + properties: + date: + type: string + format: date + name: + type: string + unit: + $ref: "#/ActivityUnit" + quantity: + description: Množství v dané jednotce + type: integer + caloriesPer100Units: + description: Spálené kcal na 100 jednotek + type: integer + nullable: true + calories: + description: Spálené kcal. Ignoruje se, pokud jde dopočíst z množství. + type: integer + nullable: true + note: + type: string + nullable: true + +ActivityDay: + description: Přehled pohybu jednoho dne včetně součtu + type: object + additionalProperties: false + required: + - date + - entries + - totalCalories + properties: + date: + type: string + format: date + entries: + description: Aktivity daného dne, nejstarší první + type: array + items: + $ref: "#/ActivityEntry" + totalCalories: + description: Součet spálených kcal za den + type: integer + +WorkoutTemplateItem: + description: Jedna položka šablony tréninku + type: object + additionalProperties: false + required: + - name + - unit + - quantity + properties: + name: + type: string + unit: + $ref: "#/ActivityUnit" + quantity: + type: integer + caloriesPer100Units: + type: integer + +WorkoutTemplate: + description: | + Předpřipravený trénink. Použitím se jeho položky založí do zvoleného dne, + takže opakovaný trénink není potřeba zadávat cvik po cviku znovu. + type: object + additionalProperties: false + required: + - id + - name + - items + - estimatedCalories + properties: + id: + type: string + name: + description: Název šablony, např. "Workout day 1" + type: string + items: + type: array + items: + $ref: "#/WorkoutTemplateItem" + estimatedCalories: + description: Odhad spálených kcal za celou šablonu + type: integer + +EnergyBalance: + description: Energetická bilance dne — kolik přišlo jídlem a kolik se vydalo + type: object + additionalProperties: false + required: + - intake + - activityBurn + - basal + - totalBurn + - balance + - hasBasal + properties: + intake: + description: Přijaté kcal z jídla + type: integer + activityBurn: + description: Spálené kcal pohybem + type: integer + basal: + description: | + Klidový výdej (bazální metabolismus) v kcal za den z nastavení. + Nula, pokud si ho uživatel nenastavil. + type: integer + totalBurn: + description: Celkový výdej, tedy klidový výdej plus pohyb + type: integer + balance: + description: | + Rozdíl příjmu a výdeje v kcal. Záporná hodnota znamená deficit, + kladná přebytek. + type: integer + hasBasal: + description: | + False, pokud uživatel nemá nastavený klidový výdej. Bilance pak + porovnává jen jídlo proti pohybu a není to skutečný deficit. + type: boolean + +DayOverview: + description: Kompletní přehled dne — jídlo, pohyb a jejich bilance + type: object + additionalProperties: false + required: + - date + - meals + - activities + - energy + properties: + date: + type: string + format: date + meals: + $ref: "#/DayRecord" + activities: + $ref: "#/ActivityDay" + energy: + $ref: "#/EnergyBalance" diff --git a/types/tsconfig.json b/types/tsconfig.json new file mode 100644 index 0000000..d3726cc --- /dev/null +++ b/types/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "noEmit": true + }, + "include": ["*.ts", "gen/**/*"] +} diff --git a/types/yarn.lock b/types/yarn.lock new file mode 100644 index 0000000..0c5bc2d --- /dev/null +++ b/types/yarn.lock @@ -0,0 +1,309 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@hey-api/client-fetch@^0.8.2": + version "0.8.4" + resolved "https://registry.yarnpkg.com/@hey-api/client-fetch/-/client-fetch-0.8.4.tgz#2dc2d368b4dd2137789f46adcae68ccd04ea2adb" + integrity sha512-SWtUjVEFIUdiJGR2NiuF0njsSrSdTe7WHWkp3BLH3DEl2bRhiflOnBo29NSDdrY90hjtTQiTQkBxUgGOF29Xzg== + +"@hey-api/json-schema-ref-parser@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.0.3.tgz#d20ed12edcbe679540af2647fb28ed61b7fbca7e" + integrity sha512-jgyNFPUReGpdB0ihWv6m+Q3dcawtXx4t6cvi0NS4xxblulcCfEjThP5xVwShFiTRScckIQ/GsuZv20arRTIDkg== + dependencies: + "@jsdevtools/ono" "^7.1.3" + "@types/json-schema" "^7.0.15" + js-yaml "^4.1.0" + +"@hey-api/openapi-ts@^0.64.7": + version "0.64.15" + resolved "https://registry.yarnpkg.com/@hey-api/openapi-ts/-/openapi-ts-0.64.15.tgz#655e8ee4039f9f190244e72595bcfee0adf43ad6" + integrity sha512-bXpi9z3YEPVt9bVqlFA3hHmgDzfM8ID5kjnXR7t6PFxlmqo8CH8Y8aCNP7rMk1q7MXk42y2CXC2ehbU9cthCyw== + dependencies: + "@hey-api/json-schema-ref-parser" "1.0.3" + c12 "2.0.1" + commander "13.0.0" + handlebars "4.7.8" + +"@jsdevtools/ono@^7.1.3": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" + integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== + +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +acorn@^8.16.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.18.0.tgz#4faf01b2d6d326bfeed97aea1f52220b5f4c1940" + integrity sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ== + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +c12@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/c12/-/c12-2.0.1.tgz#5702d280b31a08abba39833494c9b1202f0f5aec" + integrity sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A== + dependencies: + chokidar "^4.0.1" + confbox "^0.1.7" + defu "^6.1.4" + dotenv "^16.4.5" + giget "^1.2.3" + jiti "^2.3.0" + mlly "^1.7.1" + ohash "^1.1.4" + pathe "^1.1.2" + perfect-debounce "^1.0.0" + pkg-types "^1.2.0" + rc9 "^2.1.2" + +chokidar@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +citty@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/citty/-/citty-0.1.6.tgz#0f7904da1ed4625e1a9ea7e0fa780981aab7c5e4" + integrity sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ== + dependencies: + consola "^3.2.3" + +commander@13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-13.0.0.tgz#1b161f60ee3ceb8074583a0f95359a4f8701845c" + integrity sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ== + +confbox@^0.1.7, confbox@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06" + integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w== + +consola@^3.2.3, consola@^3.4.0: + version "3.4.2" + resolved "https://registry.yarnpkg.com/consola/-/consola-3.4.2.tgz#5af110145397bb67afdab77013fdc34cae590ea7" + integrity sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA== + +defu@^6.1.4: + version "6.1.7" + resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.7.tgz#72543567c8e9f97ff13ce402b6dbe09ac5ae4d23" + integrity sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ== + +destr@^2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.5.tgz#7d112ff1b925fb8d2079fac5bdb4a90973b51fdb" + integrity sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA== + +dotenv@^16.4.5: + version "16.6.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" + integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +giget@^1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/giget/-/giget-1.2.5.tgz#0bd4909356a0da75cc1f2b33538f93adec0d202f" + integrity sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug== + dependencies: + citty "^0.1.6" + consola "^3.4.0" + defu "^6.1.4" + node-fetch-native "^1.6.6" + nypm "^0.5.4" + pathe "^2.0.3" + tar "^6.2.1" + +handlebars@4.7.8: + version "4.7.8" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9" + integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.2" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + +jiti@^2.3.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.7.0.tgz#974228f2f4ca2bc21885a1797b45fea68e950c64" + integrity sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ== + +js-yaml@^4.1.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.2.tgz#8e44fb14a2643c59726bb15787b5f1512cb3d3fb" + integrity sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA== + dependencies: + argparse "^2.0.1" + +minimist@^1.2.5: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass@^3.0.0: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +mlly@^1.7.1, mlly@^1.7.4: + version "1.8.2" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.8.2.tgz#e7f7919a82d13b174405613117249a3f449d78bb" + integrity sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA== + dependencies: + acorn "^8.16.0" + pathe "^2.0.3" + pkg-types "^1.3.1" + ufo "^1.6.3" + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-fetch-native@^1.6.6: + version "1.6.7" + resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz#9d09ca63066cc48423211ed4caf5d70075d76a71" + integrity sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q== + +nypm@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/nypm/-/nypm-0.5.4.tgz#a5ab0d8d37f96342328479f88ef58699f29b3051" + integrity sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA== + dependencies: + citty "^0.1.6" + consola "^3.4.0" + pathe "^2.0.3" + pkg-types "^1.3.1" + tinyexec "^0.3.2" + ufo "^1.5.4" + +ohash@^1.1.4: + version "1.1.6" + resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.1.6.tgz#9ff7b0271d7076290794537d68ec2b40a60d133e" + integrity sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg== + +pathe@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" + integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== + +pathe@^2.0.1, pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== + +perfect-debounce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a" + integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA== + +pkg-types@^1.2.0, pkg-types@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.3.1.tgz#bd7cc70881192777eef5326c19deb46e890917df" + integrity sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ== + dependencies: + confbox "^0.1.8" + mlly "^1.7.4" + pathe "^2.0.1" + +rc9@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/rc9/-/rc9-2.1.2.tgz#6282ff638a50caa0a91a31d76af4a0b9cbd1080d" + integrity sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg== + dependencies: + defu "^6.1.4" + destr "^2.0.3" + +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== + +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +tar@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + 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" + +tinyexec@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2" + integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== + +typescript@^5.9.3: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + +ufo@^1.5.4, ufo@^1.6.3: + version "1.6.4" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.4.tgz#7a8fb875fcc6382d2c7d0b3692738b0500a92467" + integrity sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA== + +uglify-js@^3.1.4: + version "3.19.3" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f" + integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== + +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==