diff --git a/.env.example b/.env.example index 1037f43..379914f 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,7 @@ WEB_PUSH_VAPID_PRIVATE_KEY= WEB_PUSH_VAPID_SUBJECT=mailto:admin@example.com NOTIFICATIONS_CRON_SECRET= WTR_APP_URL=http://127.0.0.1:3000 +WTR_DATABASE_PATH=./data/wtr.sqlite NOTIFICATIONS_WARNING_INTERVAL_MINUTES=5 NOTIFICATIONS_MORNING_BRIEF_TIME=07:00 NOTIFICATIONS_TOMORROW_BRIEF_TIME=18:00 diff --git a/.gitignore b/.gitignore index db2fd84..b666bc5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ out npm-debug.log* .DS_Store tsconfig.tsbuildinfo +data/*.sqlite +data/*.sqlite-* diff --git a/AGENTS.md b/AGENTS.md index 5d4b8b5..96310bd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ `wtr.` to mobilna PWA pogodowa dla Polski. Pokazuje bieżącą analizę IMGW Hybrid, pomiary synoptyczne, dane hydrologiczne i ostrzeżenia z publicznych API IMGW oraz prognozę modelową łączącą IMGW ALARO z Open-Meteo. Open-Meteo Geocoding służy także do wyszukiwania miejscowości, a Nominatim / OpenStreetMap do opcjonalnego reverse geocodingu po zgodzie GPS użytkownika. -Stack: Next.js App Router, React, TypeScript, Tailwind CSS, TanStack Query, Zustand, Framer Motion, Recharts, Lucide React i `web-push`. PWA korzysta z manifestu oraz własnego service workera. +Stack: Next.js App Router, React, TypeScript, Tailwind CSS, TanStack Query, Zustand, Framer Motion, Recharts, Lucide React, `web-push` i SQLite przez `better-sqlite3`. PWA korzysta z manifestu oraz własnego service workera. Najważniejsze katalogi: @@ -50,7 +50,7 @@ Repozytorium nie ma obecnie skryptu testów ani formattera. `npm run typecheck` - Dashboard pokazuje kompaktowo wyłącznie aktywne i nadchodzące ostrzeżenia meteo dla wybranego obszaru. Filtruj je po `validTo` względem czasu przeglądarki i automatycznie usuwaj wygasłe komunikaty bez przeładowania strony. - Brief dnia i brief na jutro generuj deterministycznie w `lib/weather-brief.ts` z prognozy modelowej i ostrzeżeń IMGW. Nie traktuj ich jako odpowiedzi modelu AI i nie wymagaj klucza OpenAI API. Brief na jutro korzysta z pełnego jutrzejszego dnia prognozy i może informować o burzach na podstawie kodów modelu także bez oficjalnego ostrzeżenia IMGW. - Preferencje jednostek temperatury i wiatru są ustawieniami prezentacyjnymi. Dane źródłowe oraz progi logiki pogody pozostają liczone w `°C` i `m/s`, a wybrane jednostki zapisuj w subskrypcji Web Push, żeby briefy serwerowe używały tego samego formatu co UI. -- Powiadomienia Web Push o ostrzeżeniach meteo, porannym briefie i wieczornym briefie na jutro konfiguruj przez `/settings`, `public/sw.js`, route handlery `app/api/notifications/*`, w tym testowy `/api/notifications/test`, oraz self-hostowany worker `scripts/notification-worker.mjs`. Endpointy harmonogramu nie uruchamiają się same bez workera albo zewnętrznego crona. Wymagają kluczy VAPID w zmiennych środowiskowych. iOS/iPadOS wymaga PWA z ekranu początkowego, ale Android i desktop nie powinny być blokowane wymogiem `standalone`. Obecny `lib/push-store.ts` jest pamięciowy i przed produkcją musi zostać zastąpiony trwałym magazynem subskrypcji. +- Powiadomienia Web Push o ostrzeżeniach meteo, porannym briefie i wieczornym briefie na jutro konfiguruj przez `/settings`, `public/sw.js`, route handlery `app/api/notifications/*`, w tym testowy `/api/notifications/test`, oraz self-hostowany worker `scripts/notification-worker.mjs`. Endpointy harmonogramu nie uruchamiają się same bez workera albo zewnętrznego crona. Wymagają kluczy VAPID w zmiennych środowiskowych. iOS/iPadOS wymaga PWA z ekranu początkowego, ale Android i desktop nie powinny być blokowane wymogiem `standalone`. `lib/push-store.ts` zapisuje subskrypcje i historię wysyłek w SQLite wskazanym przez `WTR_DATABASE_PATH`, domyślnie `./data/wtr.sqlite`. - GPS wymaga świadomej zgody użytkownika i HTTPS. Zaokrąglaj współrzędne przed użyciem i utrzymuj widoczną atrybucję OpenStreetMap dla reverse geocodingu Nominatim. - Normalizuj zewnętrzne odpowiedzi i obsługuj `null`, puste pola oraz błędne wartości. Brak danych pokazuj jawnie zamiast uzupełniać estymacją. - Dla pobierania danych używaj TanStack Query z sensownym `queryKey`, cache i retry. W UI zachowuj loading, error, retry oraz empty states. diff --git a/README.md b/README.md index 2d98bab..134f65d 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Aplikacja pokazuje bieżącą analizę IMGW Hybrid, pomiary synoptyczne, prognoz - Recharts - Lucide React - `web-push` +- SQLite przez `better-sqlite3` dla trwałego magazynu subskrypcji Web Push - własny service worker i manifest PWA ## Szybki Start @@ -50,6 +51,7 @@ WEB_PUSH_VAPID_PRIVATE_KEY= WEB_PUSH_VAPID_SUBJECT=mailto:admin@example.com NOTIFICATIONS_CRON_SECRET= WTR_APP_URL=http://127.0.0.1:3000 +WTR_DATABASE_PATH=./data/wtr.sqlite NOTIFICATIONS_WARNING_INTERVAL_MINUTES=5 NOTIFICATIONS_MORNING_BRIEF_TIME=07:00 NOTIFICATIONS_TOMORROW_BRIEF_TIME=18:00 @@ -106,7 +108,7 @@ docs/ szczegółowa dokumentacja techniczna ## Status i Ograniczenia -- `lib/push-store.ts` przechowuje subskrypcje Web Push w pamięci procesu. Produkcyjne wdrożenie wymaga trwałego magazynu, np. bazy danych albo KV. +- `lib/push-store.ts` przechowuje subskrypcje Web Push i historię wysyłek w lokalnym SQLite. Na self-hostingu zadbaj o trwały katalog dla `WTR_DATABASE_PATH` i backup pliku bazy. - Endpoint IMGW Hybrid używany przez dashboard pochodzi z publicznego frontendu `meteo.imgw.pl`, a nie ze stabilnie opisanej dokumentacji `danepubliczne.imgw.pl`. - Publiczne API IMGW potrafi zwrócić `404` z komunikatem `No products were found` dla pustych list ostrzeżeń; aplikacja traktuje to jako brak ostrzeżeń. - Prognoza jest prognozą modelową, nie pomiarem IMGW. Bieżące pomiary i prognozy są w UI rozdzielane. diff --git a/docs/api.md b/docs/api.md index bc1d5bc..e7c4c9d 100644 --- a/docs/api.md +++ b/docs/api.md @@ -14,7 +14,7 @@ Wszystkie zewnętrzne źródła danych są wywoływane przez route handlery Next ## Powiadomienia -Endpointy powiadomień działają w runtime Node.js, bo korzystają z `web-push` i pamięciowego magazynu subskrypcji. +Endpointy powiadomień działają w runtime Node.js, bo korzystają z `web-push` i lokalnego magazynu SQLite dla subskrypcji. | Metoda | Endpoint | Przeznaczenie | | --- | --- | --- | diff --git a/docs/deployment.md b/docs/deployment.md index e69f6ba..50c90d3 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -60,6 +60,7 @@ WEB_PUSH_VAPID_PRIVATE_KEY= WEB_PUSH_VAPID_SUBJECT=mailto:admin@example.com NOTIFICATIONS_CRON_SECRET= WTR_APP_URL=http://127.0.0.1:3000 +WTR_DATABASE_PATH=./data/wtr.sqlite NOTIFICATIONS_WARNING_INTERVAL_MINUTES=5 NOTIFICATIONS_MORNING_BRIEF_TIME=07:00 NOTIFICATIONS_TOMORROW_BRIEF_TIME=18:00 @@ -67,6 +68,8 @@ NOTIFICATIONS_TOMORROW_BRIEF_TIME=18:00 Do zwykłego uruchomienia aplikacji pogodowej zmienne Web Push nie są wymagane. Są potrzebne dopiero dla powiadomień. +`WTR_DATABASE_PATH` wskazuje plik SQLite z subskrypcjami Web Push i historią wysyłek. Na self-hostingu ustaw go na ścieżkę w trwałym katalogu, np. `/home/admin/wtr-data/wtr.sqlite`, jeśli katalog projektu jest podmieniany podczas deploya. + ## Jakość Przed zakończeniem zmian uruchamiaj: diff --git a/docs/notifications.md b/docs/notifications.md index a20ab48..0889be3 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -31,6 +31,14 @@ Endpointy harmonogramu wymagają też sekretu crona w produkcji: NOTIFICATIONS_CRON_SECRET= ``` +Subskrypcje Web Push i historia wysyłek są zapisywane w SQLite: + +```bash +WTR_DATABASE_PATH=./data/wtr.sqlite +``` + +Jeśli zmienna nie jest ustawiona, aplikacja używa `./data/wtr.sqlite` względem katalogu projektu. + ## Preferencje Subskrypcji Subskrypcja Web Push zapisuje: @@ -114,6 +122,6 @@ x-cron-secret: ## Ograniczenia -Obecny `lib/push-store.ts` przechowuje subskrypcje i historię wysyłek w pamięci procesu. Po restarcie procesu te dane znikają. Produkcja wymaga trwałego magazynu, np. bazy danych albo KV. +`lib/push-store.ts` przechowuje subskrypcje i historię wysyłek w lokalnym SQLite przez `better-sqlite3`. Dane przetrwają restart procesu, jeśli plik wskazany przez `WTR_DATABASE_PATH` znajduje się w trwałym katalogu. Na self-hostingu warto backupować plik bazy razem z plikami `-wal` i `-shm`, jeśli istnieją. Powiadomienie testowe jest wysyłane na konkretny endpoint subskrypcji przekazany z `/settings`, a nie broadcastowane do wszystkich urządzeń. diff --git a/lib/push-store.ts b/lib/push-store.ts index fc72baf..9fd21f6 100644 --- a/lib/push-store.ts +++ b/lib/push-store.ts @@ -1,83 +1,265 @@ +import Database from "better-sqlite3"; +import fs from "node:fs"; +import path from "node:path"; +import { DEFAULT_TEMPERATURE_UNIT, DEFAULT_WIND_SPEED_UNIT, isTemperatureUnit, isWindSpeedUnit } from "@/lib/weather-utils"; import type { WarningPushSubscription } from "@/types/notifications"; +import type { Province } from "@/types/province"; -interface PushStore { - subscriptions: Map; - sentWarningIds: Map>; - sentMorningBriefDates: Map>; - sentTomorrowBriefDates: Map>; +interface PushDatabase { + database: Database.Database; } -const globalStore = globalThis as typeof globalThis & { __wtrPushStore?: PushStore }; +interface PushSubscriptionRow { + endpoint: string; + subscription_json: string; + province: string; + language: string; + enabled: number; + morning_brief_enabled: number; + tomorrow_brief_enabled: number; + latitude: number | null; + longitude: number | null; + location_name: string | null; + county_teryt: string | null; + temperature_unit: string | null; + wind_speed_unit: string | null; + created_at: string; + updated_at: string; +} -function getStore() { - if (!globalStore.__wtrPushStore) { - globalStore.__wtrPushStore = { - subscriptions: new Map(), - sentWarningIds: new Map(), - sentMorningBriefDates: new Map(), - sentTomorrowBriefDates: new Map(), - }; +const globalStore = globalThis as typeof globalThis & { __wtrPushDatabase?: PushDatabase }; + +function getDatabasePath() { + const configuredPath = process.env.WTR_DATABASE_PATH; + if (configuredPath) { + return path.isAbsolute(configuredPath) + ? configuredPath + : path.join(/* turbopackIgnore: true */ process.cwd(), configuredPath); + } + return path.join(/* turbopackIgnore: true */ process.cwd(), "data", "wtr.sqlite"); +} + +function ensureDatabaseDirectory(databasePath: string) { + fs.mkdirSync(path.dirname(databasePath), { recursive: true }); +} + +function initializeDatabase(database: Database.Database) { + database.pragma("journal_mode = WAL"); + database.pragma("foreign_keys = ON"); + database.exec(` + CREATE TABLE IF NOT EXISTS push_subscriptions ( + endpoint TEXT PRIMARY KEY, + subscription_json TEXT NOT NULL, + province TEXT NOT NULL, + language TEXT NOT NULL, + enabled INTEGER NOT NULL DEFAULT 1, + morning_brief_enabled INTEGER NOT NULL DEFAULT 0, + tomorrow_brief_enabled INTEGER NOT NULL DEFAULT 0, + latitude REAL, + longitude REAL, + location_name TEXT, + county_teryt TEXT, + temperature_unit TEXT NOT NULL DEFAULT '${DEFAULT_TEMPERATURE_UNIT}', + wind_speed_unit TEXT NOT NULL DEFAULT '${DEFAULT_WIND_SPEED_UNIT}', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS sent_warnings ( + endpoint TEXT NOT NULL, + warning_id TEXT NOT NULL, + sent_at TEXT NOT NULL, + PRIMARY KEY (endpoint, warning_id), + FOREIGN KEY (endpoint) REFERENCES push_subscriptions(endpoint) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS sent_morning_briefs ( + endpoint TEXT NOT NULL, + date_key TEXT NOT NULL, + sent_at TEXT NOT NULL, + PRIMARY KEY (endpoint, date_key), + FOREIGN KEY (endpoint) REFERENCES push_subscriptions(endpoint) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS sent_tomorrow_briefs ( + endpoint TEXT NOT NULL, + date_key TEXT NOT NULL, + sent_at TEXT NOT NULL, + PRIMARY KEY (endpoint, date_key), + FOREIGN KEY (endpoint) REFERENCES push_subscriptions(endpoint) ON DELETE CASCADE + ); + `); +} + +function getDatabase() { + if (!globalStore.__wtrPushDatabase) { + const databasePath = getDatabasePath(); + ensureDatabaseDirectory(databasePath); + const database = new Database(databasePath); + initializeDatabase(database); + globalStore.__wtrPushDatabase = { database }; + } + return globalStore.__wtrPushDatabase.database; +} + +function parseSubscription(row: PushSubscriptionRow): WarningPushSubscription | null { + try { + const subscription = JSON.parse(row.subscription_json) as WarningPushSubscription["subscription"]; + return { + endpoint: row.endpoint, + subscription, + province: row.province as Province, + language: row.language === "en" ? "en" : "pl", + enabled: row.enabled === 1, + morningBriefEnabled: row.morning_brief_enabled === 1, + tomorrowBriefEnabled: row.tomorrow_brief_enabled === 1, + latitude: row.latitude, + longitude: row.longitude, + locationName: row.location_name, + countyTeryt: row.county_teryt, + temperatureUnit: isTemperatureUnit(row.temperature_unit) ? row.temperature_unit : DEFAULT_TEMPERATURE_UNIT, + windSpeedUnit: isWindSpeedUnit(row.wind_speed_unit) ? row.wind_speed_unit : DEFAULT_WIND_SPEED_UNIT, + createdAt: row.created_at, + updatedAt: row.updated_at, + }; + } catch { + return null; } - return globalStore.__wtrPushStore; } export function upsertPushSubscription(subscription: WarningPushSubscription) { - getStore().subscriptions.set(subscription.endpoint, subscription); -} - -export function removePushSubscription(endpoint: string) { - const store = getStore(); - store.subscriptions.delete(endpoint); - store.sentWarningIds.delete(endpoint); - store.sentMorningBriefDates.delete(endpoint); - store.sentTomorrowBriefDates.delete(endpoint); -} - -export function getPushSubscriptions() { - return Array.from(getStore().subscriptions.values()); -} - -export function getPushSubscription(endpoint: string) { - return getStore().subscriptions.get(endpoint) ?? null; -} - -export function hasSentWarning(endpoint: string, warningId: string) { - return getStore().sentWarningIds.get(endpoint)?.has(warningId) ?? false; -} - -export function markWarningSent(endpoint: string, warningId: string) { - const store = getStore(); - const sentIds = store.sentWarningIds.get(endpoint) ?? new Set(); - sentIds.add(warningId); - store.sentWarningIds.set(endpoint, sentIds); -} - -export function pruneSentWarnings(activeWarningIds: Set) { - getStore().sentWarningIds.forEach((sentIds) => { - sentIds.forEach((warningId) => { - if (!activeWarningIds.has(warningId)) sentIds.delete(warningId); - }); + getDatabase().prepare(` + INSERT INTO push_subscriptions ( + endpoint, + subscription_json, + province, + language, + enabled, + morning_brief_enabled, + tomorrow_brief_enabled, + latitude, + longitude, + location_name, + county_teryt, + temperature_unit, + wind_speed_unit, + created_at, + updated_at + ) VALUES ( + @endpoint, + @subscriptionJson, + @province, + @language, + @enabled, + @morningBriefEnabled, + @tomorrowBriefEnabled, + @latitude, + @longitude, + @locationName, + @countyTeryt, + @temperatureUnit, + @windSpeedUnit, + @createdAt, + @updatedAt + ) + ON CONFLICT(endpoint) DO UPDATE SET + subscription_json = excluded.subscription_json, + province = excluded.province, + language = excluded.language, + enabled = excluded.enabled, + morning_brief_enabled = excluded.morning_brief_enabled, + tomorrow_brief_enabled = excluded.tomorrow_brief_enabled, + latitude = excluded.latitude, + longitude = excluded.longitude, + location_name = excluded.location_name, + county_teryt = excluded.county_teryt, + temperature_unit = excluded.temperature_unit, + wind_speed_unit = excluded.wind_speed_unit, + updated_at = excluded.updated_at + `).run({ + endpoint: subscription.endpoint, + subscriptionJson: JSON.stringify(subscription.subscription), + province: subscription.province, + language: subscription.language, + enabled: subscription.enabled ? 1 : 0, + morningBriefEnabled: subscription.morningBriefEnabled ? 1 : 0, + tomorrowBriefEnabled: subscription.tomorrowBriefEnabled ? 1 : 0, + latitude: subscription.latitude, + longitude: subscription.longitude, + locationName: subscription.locationName, + countyTeryt: subscription.countyTeryt, + temperatureUnit: subscription.temperatureUnit, + windSpeedUnit: subscription.windSpeedUnit, + createdAt: subscription.createdAt, + updatedAt: subscription.updatedAt, }); } +export function removePushSubscription(endpoint: string) { + getDatabase().prepare("DELETE FROM push_subscriptions WHERE endpoint = ?").run(endpoint); +} + +export function getPushSubscriptions() { + return getDatabase() + .prepare("SELECT * FROM push_subscriptions ORDER BY created_at ASC") + .all() + .map((row) => parseSubscription(row as PushSubscriptionRow)) + .filter((subscription): subscription is WarningPushSubscription => subscription !== null); +} + +export function getPushSubscription(endpoint: string) { + const row = getDatabase().prepare("SELECT * FROM push_subscriptions WHERE endpoint = ?").get(endpoint); + return row ? parseSubscription(row as PushSubscriptionRow) : null; +} + +export function hasSentWarning(endpoint: string, warningId: string) { + const row = getDatabase() + .prepare("SELECT 1 FROM sent_warnings WHERE endpoint = ? AND warning_id = ?") + .get(endpoint, warningId); + return Boolean(row); +} + +export function markWarningSent(endpoint: string, warningId: string) { + getDatabase() + .prepare("INSERT OR IGNORE INTO sent_warnings (endpoint, warning_id, sent_at) VALUES (?, ?, ?)") + .run(endpoint, warningId, new Date().toISOString()); +} + +export function pruneSentWarnings(activeWarningIds: Set) { + const database = getDatabase(); + if (!activeWarningIds.size) { + database.prepare("DELETE FROM sent_warnings").run(); + return; + } + + database.transaction((warningIds: string[]) => { + const placeholders = warningIds.map(() => "?").join(", "); + database.prepare(`DELETE FROM sent_warnings WHERE warning_id NOT IN (${placeholders})`).run(...warningIds); + })(Array.from(activeWarningIds)); +} + export function hasSentMorningBrief(endpoint: string, dateKey: string) { - return getStore().sentMorningBriefDates.get(endpoint)?.has(dateKey) ?? false; + const row = getDatabase() + .prepare("SELECT 1 FROM sent_morning_briefs WHERE endpoint = ? AND date_key = ?") + .get(endpoint, dateKey); + return Boolean(row); } export function markMorningBriefSent(endpoint: string, dateKey: string) { - const store = getStore(); - const sentDates = store.sentMorningBriefDates.get(endpoint) ?? new Set(); - sentDates.add(dateKey); - store.sentMorningBriefDates.set(endpoint, sentDates); + getDatabase() + .prepare("INSERT OR IGNORE INTO sent_morning_briefs (endpoint, date_key, sent_at) VALUES (?, ?, ?)") + .run(endpoint, dateKey, new Date().toISOString()); } export function hasSentTomorrowBrief(endpoint: string, dateKey: string) { - return getStore().sentTomorrowBriefDates.get(endpoint)?.has(dateKey) ?? false; + const row = getDatabase() + .prepare("SELECT 1 FROM sent_tomorrow_briefs WHERE endpoint = ? AND date_key = ?") + .get(endpoint, dateKey); + return Boolean(row); } export function markTomorrowBriefSent(endpoint: string, dateKey: string) { - const store = getStore(); - const sentDates = store.sentTomorrowBriefDates.get(endpoint) ?? new Set(); - sentDates.add(dateKey); - store.sentTomorrowBriefDates.set(endpoint, sentDates); + getDatabase() + .prepare("INSERT OR IGNORE INTO sent_tomorrow_briefs (endpoint, date_key, sent_at) VALUES (?, ?, ?)") + .run(endpoint, dateKey, new Date().toISOString()); } diff --git a/next.config.ts b/next.config.ts index 9cce59a..1cccaaf 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,6 +3,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { allowedDevOrigins: ["192.168.0.174"], reactStrictMode: true, + serverExternalPackages: ["better-sqlite3"], }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 10430c5..5b62d93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@tanstack/react-query": "^5.80.0", + "better-sqlite3": "^12.10.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.0.0", @@ -22,6 +23,7 @@ "zustand": "^5.0.3" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/node": "^22.10.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", @@ -1451,6 +1453,16 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -2585,6 +2597,26 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.33", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", @@ -2597,6 +2629,20 @@ "node": ">=6.0.0" } }, + "node_modules/better-sqlite3": { + "version": "12.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz", + "integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2610,6 +2656,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/bn.js": { "version": "4.12.3", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", @@ -2674,6 +2740,30 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -2825,6 +2915,12 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -3136,6 +3232,30 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3184,7 +3304,6 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -3254,6 +3373,15 @@ "dev": true, "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -3896,6 +4024,15 @@ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3970,6 +4107,12 @@ "node": ">=16.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -4078,6 +4221,12 @@ } } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4224,6 +4373,12 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4413,6 +4568,26 @@ "node": ">= 14" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4466,6 +4641,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -5214,6 +5395,18 @@ "node": ">=8.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -5242,6 +5435,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/motion-dom": { "version": "12.40.0", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.40.0.tgz", @@ -5293,6 +5492,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/napi-postinstall": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", @@ -5397,6 +5602,18 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-exports-info": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", @@ -5579,6 +5796,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5921,6 +6147,33 @@ "dev": true, "license": "MIT" }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5943,6 +6196,16 @@ "react-is": "^16.13.1" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5974,6 +6237,30 @@ ], "license": "MIT" }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "19.2.6", "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", @@ -6034,6 +6321,20 @@ "pify": "^2.3.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -6312,7 +6613,6 @@ "version": "7.8.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", - "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6514,6 +6814,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6544,6 +6889,15 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -6852,6 +7206,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -6981,6 +7363,18 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -7228,7 +7622,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/victory-vendor": { @@ -7387,6 +7780,12 @@ "node": ">=0.10.0" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 8dafd82..c29970e 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@tanstack/react-query": "^5.80.0", + "better-sqlite3": "^12.10.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.0.0", @@ -25,6 +26,7 @@ "zustand": "^5.0.3" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/node": "^22.10.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0",