From 71dfbe36cc9aa2ee5e01ccd8567126017a3ee946 Mon Sep 17 00:00:00 2001 From: null Date: Thu, 28 May 2026 20:53:22 -0500 Subject: [PATCH] refactor: component splits, PWA support, CommandPalette MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Component Splits: - AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files) - DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files) - TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal) PWA: - vite-plugin-pwa installed with NetworkFirst caching for API routes - Square PWA icons (192x192, 512x512, apple-touch-icon) - theme-color, apple meta tags, touch icon in index.html - Build generates dist/sw.js + Workbox runtime CommandPalette: - Navigation commands, Add bill action, month jumps - Grouped results with empty/filtered states --- client/components/CommandPalette.jsx | 267 +- client/components/admin/AddUserCard.jsx | 71 + client/components/admin/AuthMethodsCard.jsx | 344 ++ .../components/admin/BackupManagementCard.jsx | 381 ++ client/components/admin/CleanupPanel.jsx | 185 + client/components/admin/EmailNotifCard.jsx | 207 + client/components/admin/LoginModeCard.jsx | 135 + client/components/admin/OnboardingWizard.jsx | 160 + client/components/admin/UsersTable.jsx | 212 + client/components/admin/adminShared.jsx | 50 + .../components/data/DownloadMyDataSection.jsx | 107 + .../components/data/ImportHistorySection.jsx | 58 + .../components/data/ImportMyDataSection.jsx | 217 + .../data/ImportSpreadsheetSection.jsx | 1435 ++++++ .../data/ImportTransactionCsvSection.jsx | 568 +++ .../components/data/SeedDemoDataSection.jsx | 120 + .../data/TransactionMatchingSection.jsx | 562 +++ client/components/data/dataShared.jsx | 43 + .../components/tracker/MonthlyStateDialog.jsx | 136 + client/components/tracker/PaymentModal.jsx | 154 + .../tracker/StartingAmountsEditDialog.jsx | 197 + client/pages/AdminPage.jsx | 1858 +------- client/pages/DataPage.jsx | 3088 +----------- client/pages/TrackerPage.jsx | 440 +- client/public/img/apple-touch-icon.png | Bin 0 -> 14921 bytes client/public/img/pwa-192.png | Bin 0 -> 30114 bytes client/public/img/pwa-512.png | Bin 0 -> 117927 bytes index.html | 6 + package-lock.json | 4200 ++++++++++++++++- package.json | 5 +- vite.config.mjs | 36 +- 31 files changed, 9725 insertions(+), 5517 deletions(-) create mode 100644 client/components/admin/AddUserCard.jsx create mode 100644 client/components/admin/AuthMethodsCard.jsx create mode 100644 client/components/admin/BackupManagementCard.jsx create mode 100644 client/components/admin/CleanupPanel.jsx create mode 100644 client/components/admin/EmailNotifCard.jsx create mode 100644 client/components/admin/LoginModeCard.jsx create mode 100644 client/components/admin/OnboardingWizard.jsx create mode 100644 client/components/admin/UsersTable.jsx create mode 100644 client/components/admin/adminShared.jsx create mode 100644 client/components/data/DownloadMyDataSection.jsx create mode 100644 client/components/data/ImportHistorySection.jsx create mode 100644 client/components/data/ImportMyDataSection.jsx create mode 100644 client/components/data/ImportSpreadsheetSection.jsx create mode 100644 client/components/data/ImportTransactionCsvSection.jsx create mode 100644 client/components/data/SeedDemoDataSection.jsx create mode 100644 client/components/data/TransactionMatchingSection.jsx create mode 100644 client/components/data/dataShared.jsx create mode 100644 client/components/tracker/MonthlyStateDialog.jsx create mode 100644 client/components/tracker/PaymentModal.jsx create mode 100644 client/components/tracker/StartingAmountsEditDialog.jsx create mode 100644 client/public/img/apple-touch-icon.png create mode 100644 client/public/img/pwa-192.png create mode 100644 client/public/img/pwa-512.png diff --git a/client/components/CommandPalette.jsx b/client/components/CommandPalette.jsx index e53d73d..29a63c9 100644 --- a/client/components/CommandPalette.jsx +++ b/client/components/CommandPalette.jsx @@ -1,6 +1,9 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Loader2, Receipt, Search, X } from 'lucide-react'; +import { + BarChart2, Calendar, CreditCard, Loader2, Navigation, Plus, + Receipt, Search, Settings, Snowflake, Tag, Upload, User, X, +} from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@/api'; import { cn, fmt } from '@/lib/utils'; @@ -8,6 +11,48 @@ import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; +// ─── Navigation commands ────────────────────────────────────────────────────── + +const MONTHS = [ + 'January','February','March','April','May','June', + 'July','August','September','October','November','December', +]; + +const NAV_COMMANDS = [ + { id: 'nav-tracker', label: 'Go to Tracker', icon: Receipt, path: '/', group: 'Navigate' }, + { id: 'nav-bills', label: 'Go to Bills', icon: CreditCard, path: '/bills', group: 'Navigate' }, + { id: 'nav-calendar', label: 'Go to Calendar', icon: Calendar, path: '/calendar', group: 'Navigate' }, + { id: 'nav-summary', label: 'Go to Summary', icon: BarChart2, path: '/summary', group: 'Navigate' }, + { id: 'nav-analytics', label: 'Go to Analytics', icon: BarChart2, path: '/analytics', group: 'Navigate' }, + { id: 'nav-snowball', label: 'Go to Snowball', icon: Snowflake, path: '/snowball', group: 'Navigate' }, + { id: 'nav-categories', label: 'Go to Categories', icon: Tag, path: '/categories', group: 'Navigate' }, + { id: 'nav-data', label: 'Go to Data', icon: Upload, path: '/data', group: 'Navigate' }, + { id: 'nav-settings', label: 'Go to Settings', icon: Settings, path: '/settings', group: 'Navigate' }, + { id: 'nav-profile', label: 'Go to Profile', icon: User, path: '/profile', group: 'Navigate' }, + { id: 'action-new-bill', label: 'Add a new bill', icon: Plus, path: '/bills?new=1', group: 'Actions' }, +]; + +// Generate jump-to-month commands for the current year ± 1 +function buildMonthCommands() { + const now = new Date(); + const year = now.getFullYear(); + const commands = []; + for (let y = year - 1; y <= year + 1; y++) { + for (let m = 1; m <= 12; m++) { + commands.push({ + id: `jump-${y}-${m}`, + label: `Jump to ${MONTHS[m - 1]} ${y}`, + icon: Calendar, + path: `/?year=${y}&month=${m}`, + group: 'Jump to Month', + }); + } + } + return commands; +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── + function amountSearchText(...values) { return values .filter(value => value !== null && value !== undefined && Number.isFinite(Number(value))) @@ -42,6 +87,76 @@ function shortcutLabel() { return 'Ctrl K'; } +// ─── Result item components ─────────────────────────────────────────────────── + +function BillResult({ bill, onOpenBills, onOpenTracker }) { + return ( +
+ +
+ + +
+
+ ); +} + +function CommandResult({ cmd, onRun }) { + const Icon = cmd.icon || Navigation; + return ( + + ); +} + +// ─── Main component ─────────────────────────────────────────────────────────── + export default function CommandPalette() { const navigate = useNavigate(); const inputRef = useRef(null); @@ -50,6 +165,7 @@ export default function CommandPalette() { const [bills, setBills] = useState([]); const [loading, setLoading] = useState(false); const [loaded, setLoaded] = useState(false); + const monthCommands = useMemo(() => buildMonthCommands(), []); useEffect(() => { const openPalette = () => setOpen(true); @@ -85,18 +201,6 @@ export default function CommandPalette() { .finally(() => setLoading(false)); }, [loaded, loading, open]); - const results = useMemo(() => { - const q = query.trim().toLowerCase(); - const sorted = [...bills].sort((a, b) => { - if (!!a.active !== !!b.active) return a.active ? -1 : 1; - return String(a.name || '').localeCompare(String(b.name || '')); - }); - if (!q) return sorted.slice(0, 8); - return sorted - .filter(bill => billSearchText(bill).includes(q)) - .slice(0, 12); - }, [bills, query]); - const close = () => { setOpen(false); setQuery(''); @@ -115,6 +219,46 @@ export default function CommandPalette() { close(); }; + const runCommand = (cmd) => { + navigate(cmd.path); + close(); + }; + + const allCommands = useMemo(() => [...NAV_COMMANDS, ...monthCommands], [monthCommands]); + + const { billResults, commandResults } = useMemo(() => { + const q = query.trim().toLowerCase(); + + if (!q) { + const sortedBills = [...bills] + .sort((a, b) => { + if (!!a.active !== !!b.active) return a.active ? -1 : 1; + return String(a.name || '').localeCompare(String(b.name || '')); + }) + .slice(0, 6); + return { + billResults: sortedBills, + commandResults: NAV_COMMANDS.slice(0, 4), + }; + } + + const matchedBills = [...bills] + .filter(bill => billSearchText(bill).includes(q)) + .sort((a, b) => { + if (!!a.active !== !!b.active) return a.active ? -1 : 1; + return String(a.name || '').localeCompare(String(b.name || '')); + }) + .slice(0, 8); + + const matchedCommands = allCommands + .filter(cmd => cmd.label.toLowerCase().includes(q) || cmd.group.toLowerCase().includes(q)) + .slice(0, 5); + + return { billResults: matchedBills, commandResults: matchedCommands }; + }, [bills, query, allCommands]); + + const hasResults = billResults.length > 0 || commandResults.length > 0; + return ( @@ -129,9 +273,12 @@ export default function CommandPalette() { value={query} onChange={event => setQuery(event.target.value)} onKeyDown={event => { - if (event.key === 'Enter' && results[0]) openBills(results[0]); + if (event.key === 'Enter') { + if (billResults[0]) openBills(billResults[0]); + else if (commandResults[0]) runCommand(commandResults[0]); + } }} - placeholder="Find a bill by name, category, notes, or amount" + placeholder="Search bills or type a command…" className="h-9 border-0 bg-transparent px-0 text-base shadow-none focus-visible:ring-0" /> {query && ( @@ -148,66 +295,56 @@ export default function CommandPalette() { )} -
+
{loading ? (
- Loading bills... + Loading…
- ) : results.length > 0 ? ( -
- {results.map(bill => ( -
- -
- - -
-
- ))} + ) : !hasResults ? ( +
+ No results.
) : ( -
- No bills found. -
+ <> + {commandResults.length > 0 && ( +
+

+ Commands +

+
+ {commandResults.map(cmd => ( + + ))} +
+
+ )} + + {billResults.length > 0 && ( +
+ {commandResults.length > 0 && ( +

+ Bills +

+ )} +
+ {billResults.map(bill => ( + + ))} +
+
+ )} + )}
- Enter opens Bills + Enter to open · Tab to focus {shortcutLabel()}
diff --git a/client/components/admin/AddUserCard.jsx b/client/components/admin/AddUserCard.jsx new file mode 100644 index 0000000..ccb2c92 --- /dev/null +++ b/client/components/admin/AddUserCard.jsx @@ -0,0 +1,71 @@ +import React, { useState } from 'react'; +import { toast } from 'sonner'; +import { api } from '@/api'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; + +export default function AddUserCard({ onCreated }) { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleCreate = async (e) => { + e.preventDefault(); + setError(''); + + if (password.length < 8) { + const msg = 'Password must be at least 8 characters.'; + setError(msg); + toast.error(msg); + return; + } + + setLoading(true); + try { + await api.createUser({ username, password }); + toast.success(`User "${username}" created.`); + setUsername(''); + setPassword(''); + setError(''); + onCreated(); + } catch (err) { + const errorMessage = err.message || 'Failed to create user.'; + setError(errorMessage); + toast.error(errorMessage); + } finally { + setLoading(false); + } + }; + + return ( + + + Add User + + +
+
+ + setUsername(e.target.value)} placeholder="username" required /> +
+
+ + setPassword(e.target.value)} placeholder="Password" required /> +
+
+ {error && ( +
+ {error} +
+ )} + + + + + ); +} diff --git a/client/components/admin/AuthMethodsCard.jsx b/client/components/admin/AuthMethodsCard.jsx new file mode 100644 index 0000000..aac86fc --- /dev/null +++ b/client/components/admin/AuthMethodsCard.jsx @@ -0,0 +1,344 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { toast } from 'sonner'; +import { api } from '@/api'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { FieldRow, Toggle } from './adminShared'; + +const AUTHENTIK_ICON_URL = '/img/auth.png'; + +function defaultOidcRedirectUri() { + if (typeof window === 'undefined') return ''; + return `${window.location.origin}/api/auth/oidc/callback`; +} + +function looksLikeOidcEndpoint(url) { + const value = String(url || '').toLowerCase(); + return /\/(?:authorize|token|userinfo|jwks|certs)\/?$/.test(value); +} + +export default function AuthMethodsCard() { + const [data, setData] = useState(null); + const [form, setForm] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [testingOidc, setTestingOidc] = useState(false); + const [oidcTest, setOidcTest] = useState(null); + + const load = useCallback(async () => { + try { + const d = await api.authModeConfig(); + setData(d); + setForm({ + local_login_enabled: d.local_login_enabled !== false, + oidc_login_enabled: !!d.oidc_login_enabled, + oidc_provider_name: d.oidc_provider_name || 'authentik', + oidc_issuer_url: d.oidc_issuer_url || '', + oidc_client_id: d.oidc_client_id || '', + oidc_client_secret: '', + oidc_client_secret_clear: false, + oidc_token_auth_method: d.oidc_token_auth_method || 'client_secret_basic', + oidc_redirect_uri: d.oidc_redirect_uri || defaultOidcRedirectUri(), + oidc_scopes: d.oidc_scopes || 'openid email profile groups', + oidc_auto_provision: d.oidc_auto_provision !== false, + oidc_admin_group: d.oidc_admin_group || '', + oidc_default_role: d.oidc_default_role || 'user', + }); + } catch (err) { + toast.error(err.message || 'Failed to load auth settings.'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { load(); }, [load]); + + const set = (k, v) => setForm(prev => ({ ...prev, [k]: v })); + + async function handleSave() { + setSaving(true); + try { + const d = await api.setAuthMode(form); + setData(d); + setForm({ + local_login_enabled: d.local_login_enabled !== false, + oidc_login_enabled: !!d.oidc_login_enabled, + oidc_provider_name: d.oidc_provider_name || 'authentik', + oidc_issuer_url: d.oidc_issuer_url || '', + oidc_client_id: d.oidc_client_id || '', + oidc_client_secret: '', + oidc_client_secret_clear: false, + oidc_token_auth_method: d.oidc_token_auth_method || 'client_secret_basic', + oidc_redirect_uri: d.oidc_redirect_uri || defaultOidcRedirectUri(), + oidc_scopes: d.oidc_scopes || 'openid email profile groups', + oidc_auto_provision: d.oidc_auto_provision !== false, + oidc_admin_group: d.oidc_admin_group || '', + oidc_default_role: d.oidc_default_role || 'user', + }); + toast.success('Auth method settings saved.'); + } catch (err) { + toast.error(err.message || 'Failed to save auth method settings.'); + } finally { + setSaving(false); + } + } + + async function handleTestOidc() { + setTestingOidc(true); + setOidcTest(null); + try { + const result = await api.testOidcConfig(form); + setOidcTest(result); + toast.success('authentik configuration test passed.'); + } catch (err) { + const result = err.data || { ok: false, error: err.message || 'OIDC configuration test failed.' }; + setOidcTest(result); + toast.error(result.error || 'OIDC configuration test failed.'); + } finally { + setTestingOidc(false); + } + } + + if (loading || !form) { + return ( + + + Loading auth settings… + + + ); + } + + const secretAvailable = form.oidc_client_secret.trim() + ? true + : form.oidc_client_secret_clear + ? false + : !!data?.oidc_client_secret_set; + const oidcConfigured = !!( + form.oidc_issuer_url.trim() && + form.oidc_client_id.trim() && + secretAvailable && + form.oidc_redirect_uri.trim() + ); + const adminGroupConfigured = !!form.oidc_admin_group.trim(); + const wouldLockOut = !form.local_login_enabled && !form.oidc_login_enabled; + const cantDisableLocal = !form.local_login_enabled && (!oidcConfigured || !form.oidc_login_enabled || !adminGroupConfigured); + const oidcEnabledButIncomplete = form.oidc_login_enabled && !oidcConfigured; + const canSave = !wouldLockOut && !cantDisableLocal && !oidcEnabledButIncomplete && !saving; + const canTestOidc = oidcConfigured && !testingOidc; + const missingFields = [ + !form.oidc_issuer_url.trim() && 'Issuer URL', + !form.oidc_client_id.trim() && 'Client ID', + !secretAvailable && 'Client Secret', + !form.oidc_redirect_uri.trim() && 'Redirect URI', + ].filter(Boolean); + const issuerEndpointWarning = looksLikeOidcEndpoint(form.oidc_issuer_url); + + return ( + + +
+
+ Authentication Methods +

+ Control local login and authentik/OIDC. Settings are saved in the database; + environment variables only fill blank fields as bootstrap defaults. +

+
+
+
+ + + + {(data?.warnings?.length > 0 || wouldLockOut || cantDisableLocal) && ( +
+ {wouldLockOut && ( +

+ Cannot disable all login methods; at least one must remain enabled. +

+ )} + {cantDisableLocal && !wouldLockOut && ( +

+ Cannot disable local login without authentik/OIDC configured, enabled, and mapped to an admin group. +

+ )} + {oidcEnabledButIncomplete && ( +

+ authentik/OIDC needs {missingFields.join(', ')} before it can be enabled. +

+ )} + {data?.warnings?.map((w, i) => ( +

{w}

+ ))} +
+ )} + + +
+ set('local_login_enabled', v)} label="Enable local login" /> + {form.local_login_enabled ? 'Enabled' : 'Disabled'} +
+
+ + +
+ set('oidc_login_enabled', v)} label="Enable OIDC login" /> + + {!oidcConfigured ? 'Not fully configured' : form.oidc_login_enabled ? 'Enabled' : 'Disabled'} + +
+
+ +
+
+ + authentik / OIDC configuration +
+ + + set('oidc_provider_name', e.target.value)} placeholder="authentik" className="max-w-xs h-8 text-sm" /> + + + +
+ set('oidc_issuer_url', e.target.value)} + placeholder="https://yourURL.com/application/o/bills/.well-known/openid-configuration" + className="max-w-xl h-8 text-sm" + /> +

+ Use the authentik provider issuer URL or full discovery URL, for example https://yourURL.com/application/o/bills/.well-known/openid-configuration. +

+ {issuerEndpointWarning && ( +

+ This looks like an authorization endpoint. In authentik, copy the provider issuer or OpenID Configuration URL. +

+ )} +
+
+ + + set('oidc_client_id', e.target.value)} placeholder="authentik client ID" className="max-w-xl h-8 text-sm" /> + + + +
+
+ setForm(prev => ({ + ...prev, + oidc_client_secret: e.target.value, + oidc_client_secret_clear: e.target.value ? false : prev.oidc_client_secret_clear, + }))} + placeholder="Leave blank to keep existing secret" + className="h-8 text-sm" + /> + + {data?.oidc_client_secret_set && !form.oidc_client_secret_clear ? 'Secret is set' : 'No secret saved'} + +
+ +
+
+ + +
+ +

+ Advanced. Keep client_secret_basic unless your authentik provider explicitly requires client_secret_post. +

+
+
+ + +
+
+ set('oidc_redirect_uri', e.target.value)} placeholder={defaultOidcRedirectUri()} className="h-8 text-sm" /> + +
+

Add this exact URL to the Redirect URIs allowed by authentik.

+
+
+ + + set('oidc_scopes', e.target.value)} placeholder="openid email profile groups" className="max-w-xl h-8 text-sm" /> + + + +
+ set('oidc_admin_group', e.target.value)} placeholder="e.g. bill-tracker-admins" className="max-w-sm h-8 text-sm" /> +

Only users in this authentik group become app admins. Admin is never granted by default.

+
+
+ + +
+
+ set('oidc_auto_provision', v)} label="Auto-provision users" /> + {form.oidc_auto_provision ? 'Enabled' : 'Disabled'} +
+

When enabled, valid authentik users are created in this app on first login.

+
+
+ + +
+ + Admin role only via admin group. +
+
+ + {data?.oidc_env_fallback_used && ( +
+ One or more blank database fields are currently using environment fallback values. Saving values here takes precedence. +
+ )} + + {oidcTest && ( +
+ {oidcTest.ok + ? `Configuration test passed for ${oidcTest.issuer || form.oidc_issuer_url}.` + : oidcTest.error || 'Configuration test failed.'} +
+ )} +
+ +
+ + + +
+ +
+
+ ); +} diff --git a/client/components/admin/BackupManagementCard.jsx b/client/components/admin/BackupManagementCard.jsx new file mode 100644 index 0000000..fa31a6d --- /dev/null +++ b/client/components/admin/BackupManagementCard.jsx @@ -0,0 +1,381 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { Database, Download, Play, RefreshCw, RotateCcw, Trash2, Upload } from 'lucide-react'; +import { toast } from 'sonner'; +import { api } from '@/api'; +import { cn, fmtBytes } from '@/lib/utils'; +import { Button, buttonVariants } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { + Select, SelectTrigger, SelectValue, SelectContent, SelectItem, +} from '@/components/ui/select'; +import { + AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, + AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, +} from '@/components/ui/alert-dialog'; +import { SectionHeading, Toggle, formatDateTime, BackupTypeBadge } from './adminShared'; + +const DEFAULT_SETTINGS = { + enabled: false, + frequency: 'daily', + time: '02:00', + retention_count: 14, + last_run_at: null, + next_run_at: null, + last_error: null, +}; + +export default function BackupManagementCard() { + const [backups, setBackups] = useState([]); + const [settings, setSettings] = useState(DEFAULT_SETTINGS); + const [loading, setLoading] = useState(true); + const [busy, setBusy] = useState(''); + const [restoreTarget, setRestoreTarget] = useState(null); + const [deleteTarget, setDeleteTarget] = useState(null); + + const load = useCallback(async () => { + setLoading(true); + try { + const [backupData, settingsData] = await Promise.all([ + api.adminBackups(), + api.adminBackupSettings(), + ]); + setBackups(backupData.backups || []); + setSettings({ ...DEFAULT_SETTINGS, ...settingsData }); + } catch (err) { + toast.error(err.message || 'Failed to load backups.'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { load(); }, [load]); + + const latest = backups[0]; + const setSchedule = (key, value) => setSettings(prev => ({ ...prev, [key]: value })); + + async function handleCreate() { + setBusy('create'); + try { + await api.createAdminBackup(); + toast.success('Backup created.'); + await load(); + } catch (err) { + toast.error(err.message || 'Failed to create backup.'); + } finally { + setBusy(''); + } + } + + async function handleDownload(backup) { + setBusy(`download:${backup.id}`); + try { + const { blob, filename } = await api.downloadAdminBackup(backup.id); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename || backup.id; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + } catch (err) { + toast.error(err.message || 'Failed to download backup.'); + } finally { + setBusy(''); + } + } + + async function handleImport(e) { + const file = e.target.files?.[0]; + e.target.value = ''; + if (!file) return; + + setBusy('import'); + try { + await api.importAdminBackup(file); + toast.success('Backup imported.'); + await load(); + } catch (err) { + toast.error(err.message || 'Failed to import backup.'); + } finally { + setBusy(''); + } + } + + async function handleRestore() { + if (!restoreTarget) return; + setBusy(`restore:${restoreTarget.id}`); + try { + const result = await api.restoreAdminBackup(restoreTarget.id); + toast.success(`Database restored. Pre-restore backup: ${result.pre_restore_backup}`); + setRestoreTarget(null); + await load(); + } catch (err) { + toast.error(err.message || 'Failed to restore backup.'); + } finally { + setBusy(''); + } + } + + async function handleDelete() { + if (!deleteTarget) return; + setBusy(`delete:${deleteTarget.id}`); + try { + await api.deleteAdminBackup(deleteTarget.id); + toast.success('Backup deleted.'); + setDeleteTarget(null); + await load(); + } catch (err) { + toast.error(err.message || 'Failed to delete backup.'); + } finally { + setBusy(''); + } + } + + async function handleSaveSettings() { + setBusy('settings'); + try { + const saved = await api.saveAdminBackupSettings({ + enabled: !!settings.enabled, + frequency: settings.frequency, + time: settings.time, + retention_count: parseInt(settings.retention_count, 10) || 14, + }); + setSettings({ ...DEFAULT_SETTINGS, ...saved }); + toast.success('Backup schedule saved.'); + } catch (err) { + toast.error(err.message || 'Failed to save backup schedule.'); + } finally { + setBusy(''); + } + } + + async function handleRunScheduledNow() { + setBusy('run-scheduled'); + try { + await api.runScheduledBackupNow(); + toast.success('Scheduled backup created.'); + await load(); + } catch (err) { + toast.error(err.message || 'Failed to run scheduled backup.'); + } finally { + setBusy(''); + } + } + + if (loading) { + return Loading backups…; + } + + return ( + <> + + +
+
+ Database Backups +

+ Admin-only SQLite backup, import, download, restore, and schedule controls. +

+
+ + {settings.last_error ? 'Attention' : 'Ready'} + +
+
+ +
+ {[ + ['Managed backups', backups.length], + ['Latest backup', latest ? formatDateTime(latest.modified_at) : '—'], + ['Latest size', latest ? fmtBytes(latest.size_bytes) : '—'], + ['Scheduled', settings.enabled ? `${settings.frequency} ${settings.time}` : 'Disabled'], + ].map(([label, value]) => ( +
+

{label}

+

{value}

+
+ ))} +
+ + {settings.last_error && ( +
+ {settings.last_error} +
+ )} + +
+ + + +
+ +
+ + + + + + + + + + + + {backups.map(backup => ( + + + + + + + + + ))} + {!backups.length && ( + + + + )} + +
BackupTypeModifiedSizeChecksum +
{backup.id}{formatDateTime(backup.modified_at)}{fmtBytes(backup.size_bytes)} + {backup.checksum || '—'} + +
+ + + +
+
No managed backups yet.
+
+ +
+
+
+ Scheduled Backups +

+ Scheduled runs create managed backups and only apply retention to scheduled backups. +

+
+ setSchedule('enabled', v)} label="Enable scheduled backups" /> +
+ +
+
+ + +
+
+ + setSchedule('time', e.target.value)} /> +
+
+ + setSchedule('retention_count', e.target.value)} /> +
+
+ +
+ {formatDateTime(settings.next_run_at)} +
+
+
+ +
+
Last run: {formatDateTime(settings.last_run_at)}
+
Last error: {settings.last_error || '—'}
+
+ +
+ + +
+
+
+
+ + { if (!open) setRestoreTarget(null); }}> + + + Restore this database backup? + + This replaces the live database with {restoreTarget?.id}. + A pre-restore backup will be created first. Run this during a quiet maintenance window. + + + + Cancel + + {busy.startsWith('restore:') ? 'Restoring…' : 'Restore Database'} + + + + + + { if (!open) setDeleteTarget(null); }}> + + + Delete this backup? + + This permanently deletes {deleteTarget?.id}. + The live database is not affected. + + + + Cancel + + {busy.startsWith('delete:') ? 'Deleting…' : 'Delete Backup'} + + + + + + ); +} diff --git a/client/components/admin/CleanupPanel.jsx b/client/components/admin/CleanupPanel.jsx new file mode 100644 index 0000000..68ba7dc --- /dev/null +++ b/client/components/admin/CleanupPanel.jsx @@ -0,0 +1,185 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { Play, Wrench } from 'lucide-react'; +import { toast } from 'sonner'; +import { api } from '@/api'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Input } from '@/components/ui/input'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { FieldRow, Toggle, formatDateTime } from './adminShared'; + +export default function CleanupPanel() { + const [status, setStatus] = useState(null); + const [form, setForm] = useState({ + import_sessions_enabled: true, + temp_exports_enabled: true, + temp_export_max_age_hours: 2, + backup_partials_enabled: true, + import_history_enabled: false, + import_history_max_age_days: 365, + }); + const [saving, setSaving] = useState(false); + const [running, setRunning] = useState(false); + const [loading, setLoading] = useState(true); + + const load = useCallback(async () => { + try { + const data = await api.adminCleanup(); + setStatus(data); + if (data.settings) setForm(data.settings); + } catch (err) { + toast.error(err.message || 'Failed to load cleanup settings.'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { load(); }, [load]); + + const set = (k, v) => setForm(prev => ({ ...prev, [k]: v })); + + async function handleSave() { + setSaving(true); + try { + const next = await api.saveAdminCleanup(form); + if (next) setForm(next); + toast.success('Cleanup settings saved.'); + } catch (err) { + toast.error(err.message || 'Failed to save cleanup settings.'); + } finally { + setSaving(false); + } + } + + async function handleRunNow() { + setRunning(true); + try { + const result = await api.runAdminCleanup(); + setStatus(prev => ({ + ...prev, + last_run_at: result.ran_at, + last_result: result.tasks, + })); + toast.success('Cleanup tasks completed.'); + } catch (err) { + toast.error(err.message || 'Cleanup run failed.'); + } finally { + setRunning(false); + } + } + + function resultLine(label, task, countKey) { + if (!task || task[countKey] == null) return null; + return `${label}: ${task[countKey]}`; + } + + const resultLines = status?.last_result ? [ + resultLine('Import sessions pruned', status.last_result.import_sessions, 'pruned'), + resultLine('Temp export files removed', status.last_result.temp_export_files, 'removed'), + resultLine('Backup partials removed', status.last_result.backup_partials, 'removed'), + resultLine('Import history rows pruned', status.last_result.import_history, 'pruned'), + ].filter(Boolean) : []; + + if (loading) { + return ( + + + Loading cleanup settings… + + + ); + } + + return ( + + +
+
+ +
+ Cleanup / Maintenance +

+ Automatic daily cleanup: expired import sessions, stale export temp files, orphaned backup partials. Runs at 6:00 AM. +

+
+
+ Auto +
+
+ + +
+
+

Last Run

+

{status?.last_run_at ? formatDateTime(status.last_run_at) : '—'}

+
+
+

Last Result

+ {resultLines.length > 0 ? ( +
    + {resultLines.map(line => ( +
  • {line}
  • + ))} +
+ ) : ( +

No runs recorded yet

+ )} +
+
+ +
+

Task Settings

+ {[ + ['import_sessions_enabled', 'Prune expired import sessions (24h TTL)'], + ['temp_exports_enabled', 'Remove stale SQLite export temp files'], + ['backup_partials_enabled', 'Remove orphaned backup .partial / .upload files'], + ].map(([key, label]) => ( + + set(key, v)} label={label} /> + + ))} + + + set('temp_export_max_age_hours', parseInt(e.target.value, 10) || 2)} + disabled={!form.temp_exports_enabled} + className="max-w-[120px] h-8 text-sm" + /> + + + + set('import_history_enabled', v)} label="Trim import history rows" /> + + + {form.import_history_enabled && ( + <> + + set('import_history_max_age_days', parseInt(e.target.value, 10) || 365)} + className="max-w-[120px] h-8 text-sm" + /> + +
+ Warning: Import history trimming permanently deletes audit records older than {form.import_history_max_age_days} days. This cannot be undone. +
+ + )} +
+ +
+ + +
+
+
+ ); +} diff --git a/client/components/admin/EmailNotifCard.jsx b/client/components/admin/EmailNotifCard.jsx new file mode 100644 index 0000000..81c626e --- /dev/null +++ b/client/components/admin/EmailNotifCard.jsx @@ -0,0 +1,207 @@ +import React, { useState, useEffect } from 'react'; +import { Eye, EyeOff } from 'lucide-react'; +import { toast } from 'sonner'; +import { api } from '@/api'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { + Select, SelectTrigger, SelectValue, SelectContent, SelectItem, +} from '@/components/ui/select'; +import { SectionHeading, FieldRow, Toggle } from './adminShared'; + +export default function EmailNotifCard() { + const DEFAULTS = { + enabled: false, + sender_name: '', sender_address: '', + smtp_host: '', smtp_port: '587', smtp_encryption: 'starttls', + smtp_self_signed: false, + smtp_username: '', smtp_password: '', + allow_user_config: false, + global_recipient: '', + }; + + const [cfg, setCfg] = useState(DEFAULTS); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [showPw, setShowPw] = useState(false); + const [testEmail, setTestEmail] = useState(''); + const [testing, setTesting] = useState(false); + + useEffect(() => { + api.notifAdmin() + .then(d => setCfg({ ...DEFAULTS, ...d })) + .catch(() => {}) + .finally(() => setLoading(false)); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const set = (k, v) => setCfg(p => ({ ...p, [k]: v })); + + const handleSave = async () => { + setSaving(true); + try { + await api.saveNotifAdmin(cfg); + toast.success('Email settings saved.'); + } catch (err) { + toast.error(err.message || 'Failed to save.'); + } finally { + setSaving(false); + } + }; + + const handleTest = async () => { + if (!testEmail) { toast.error('Enter a recipient email.'); return; } + setTesting(true); + try { + await api.testEmail({ to: testEmail }); + toast.success('Test email sent.'); + } catch (err) { + toast.error(err.message || 'Failed to send test email.'); + } finally { + setTesting(false); + } + }; + + if (loading) return Loading…; + + return ( + + + Email Notifications + + +
+
+

Enable email notifications

+

Configure SMTP to send bill reminders

+
+ set('enabled', v)} label="Enable email notifications" /> +
+ +
+ +
+ Sender + + set('sender_name', e.target.value)} placeholder="BillTracker" /> + + + set('sender_address', e.target.value)} placeholder="no-reply@example.com" type="email" /> + +
+ +
+ +
+ SMTP Server + + set('smtp_host', e.target.value)} placeholder="smtp.example.com" /> + + + set('smtp_port', e.target.value)} placeholder="587" type="number" className="w-28" /> + + + + + +
+ set('smtp_self_signed', e.target.checked)} + className="h-4 w-4 rounded border-input bg-input accent-primary" + /> + +
+
+ + set('smtp_username', e.target.value)} placeholder="user@example.com" /> + + +
+ set('smtp_password', e.target.value)} + placeholder="••••••••" + className="pr-9" + /> + +
+
+
+ +
+ +
+ User Access + +
+ set('allow_user_config', e.target.checked)} + className="h-4 w-4 rounded border-input bg-input accent-primary" + /> + +
+
+ + set('global_recipient', e.target.value)} + placeholder="recipient@example.com" + type="email" + /> + +
+ +
+ +
+ Test Email + +
+ setTestEmail(e.target.value)} + placeholder="you@example.com" + type="email" + /> + +
+
+
+ +
+ +
+ + + ); +} diff --git a/client/components/admin/LoginModeCard.jsx b/client/components/admin/LoginModeCard.jsx new file mode 100644 index 0000000..6392937 --- /dev/null +++ b/client/components/admin/LoginModeCard.jsx @@ -0,0 +1,135 @@ +import React, { useState, useEffect } from 'react'; +import { toast } from 'sonner'; +import { api } from '@/api'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Label } from '@/components/ui/label'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { + Select, SelectTrigger, SelectValue, SelectContent, SelectItem, +} from '@/components/ui/select'; +import { + AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, + AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, +} from '@/components/ui/alert-dialog'; + +export default function LoginModeCard({ users }) { + const [modeData, setModeData] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [selectedUser, setSelectedUser] = useState(''); + + const [confirmSingle, setConfirmSingle] = useState(false); + const [pendingUserId, setPendingUserId] = useState(null); + + useEffect(() => { + api.authModeConfig() + .then(d => { setModeData(d); setSelectedUser(d.default_user_id?.toString() || ''); }) + .catch(() => {}) + .finally(() => setLoading(false)); + }, []); + + const doSetMode = async (mode, userId) => { + setSaving(true); + try { + await api.setAuthMode({ + auth_mode: mode, + default_user_id: mode === 'single' ? parseInt(userId, 10) : null, + }); + const d = await api.authModeConfig(); + setModeData(d); + toast.success(mode === 'single' ? 'Single-user mode enabled.' : 'Login requirement restored.'); + } catch (err) { + toast.error(err.message || 'Failed to update auth mode.'); + } finally { + setSaving(false); + } + }; + + const handleRequestSingle = () => { + if (!selectedUser) { toast.error('Select a user first.'); return; } + setPendingUserId(selectedUser); + setConfirmSingle(true); + }; + + const handleConfirmSingle = () => { + setConfirmSingle(false); + doSetMode('single', pendingUserId); + }; + + if (loading) return Loading…; + + const isMulti = !modeData || modeData.auth_mode === 'multi'; + const activeUser = users?.find(u => u.id === modeData?.default_user_id); + const selectedUsername = users?.find(u => u.id.toString() === selectedUser)?.username ?? selectedUser; + + return ( + <> + + +
+ Login Mode + + {isMulti ? 'Multi-user' : 'Single-user'} + +
+
+ + {isMulti ? ( + <> +

+ Single-user mode bypasses the login screen and automatically signs in as the selected user. +

+
+ + +
+ + + ) : ( + <> +

+ Currently auto-signing in as{' '} + {activeUser?.username ?? '—'}. + Restoring login requirement will require all users to sign in manually. +

+ + + )} +
+
+ + + + + Enable Single-User Mode? + + Anyone who opens the app will be automatically signed in as{' '} + {selectedUsername}. + The admin login still requires a password. + + + + Cancel + + Enable Single-User Mode + + + + + + ); +} diff --git a/client/components/admin/OnboardingWizard.jsx b/client/components/admin/OnboardingWizard.jsx new file mode 100644 index 0000000..7944df2 --- /dev/null +++ b/client/components/admin/OnboardingWizard.jsx @@ -0,0 +1,160 @@ +import React, { useState } from 'react'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; +import { toast } from 'sonner'; +import { api } from '@/api'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; + +export default function OnboardingWizard({ onComplete }) { + const [step, setStep] = useState(0); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [confirm, setConfirm] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleCreate = async (e) => { + e.preventDefault(); + setError(''); + + let validationError = ''; + if (password !== confirm) { + validationError = 'Passwords do not match.'; + } else if (password.length < 8) { + validationError = 'Password must be at least 8 characters.'; + } + + if (validationError) { + setError(validationError); + toast.error(validationError); + return; + } + + setLoading(true); + try { + await api.createUser({ username, password }); + toast.success('User created successfully.'); + onComplete(); + } catch (err) { + const errorMessage = err.message || 'Failed to create user.'; + setError(errorMessage); + toast.error(errorMessage); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+ {[0, 1].map(i => ( + + ))} +
+ + {step === 0 && ( + + + Welcome, Administrator +

+ Before creating your first user, please understand what your admin account can and cannot do. +

+
+ +
+ {[ + { can: true, text: 'Create and manage user accounts' }, + { can: true, text: 'Reset user passwords' }, + { can: true, text: 'Configure email notifications' }, + { can: true, text: 'Toggle single-user / multi-user mode' }, + { can: false, text: 'Cannot view bills or financial data' }, + { can: false, text: 'Cannot access user settings or history' }, + ].map(({ can, text }) => ( +
+ + {can ? '✓' : '✗'} + + {text} +
+ ))} +
+
+ +
+
+
+ )} + + {step === 1 && ( + + + Create first user +

+ This account will be used to access the bill tracker. +

+
+ +
+
+ + setUsername(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+
+ + setConfirm(e.target.value)} + required + /> +
+ {error && ( +
+ {error} +
+ )} +
+ + +
+
+
+
+ )} +
+
+ ); +} diff --git a/client/components/admin/UsersTable.jsx b/client/components/admin/UsersTable.jsx new file mode 100644 index 0000000..ab692fd --- /dev/null +++ b/client/components/admin/UsersTable.jsx @@ -0,0 +1,212 @@ +import React, { useState } from 'react'; +import { toast } from 'sonner'; +import { api } from '@/api'; +import { cn } from '@/lib/utils'; +import { Button, buttonVariants } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { + AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, + AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, +} from '@/components/ui/alert-dialog'; + +export default function UsersTable({ users, onRefresh, currentUser }) { + const [resetForms, setResetForms] = useState({}); + const [deleting, setDeleting] = useState(null); + const [resetting, setResetting] = useState(null); + const [roleUpdating, setRoleUpdating] = useState(null); + const [activeUpdating, setActiveUpdating] = useState(null); + const [deleteTarget, setDeleteTarget] = useState(null); + + const setReset = (id, v) => setResetForms(p => ({ ...p, [id]: { ...(p[id] || {}), ...v } })); + const getForm = (id) => resetForms[id] || { pw: '', open: false }; + + const handleReset = async (user) => { + const form = getForm(user.id); + if (!form.pw || form.pw.length < 8) { toast.error('Password must be at least 8 characters.'); return; } + setResetting(user.id); + try { + await api.resetPassword(user.id, { password: form.pw }); + toast.success(`Password reset for ${user.username}.`); + setReset(user.id, { pw: '', open: false }); + } catch (err) { + toast.error(err.message || 'Failed to reset password.'); + } finally { + setResetting(null); + } + }; + + const handleDelete = async () => { + if (!deleteTarget) return; + setDeleting(deleteTarget.id); + try { + await api.deleteUser(deleteTarget.id); + toast.success(`User ${deleteTarget.username} deleted.`); + setDeleteTarget(null); + onRefresh(); + } catch (err) { + toast.error(err.message || 'Failed to delete user.'); + } finally { + setDeleting(null); + } + }; + + const handleRoleChange = async (user, role) => { + if (user.role === role) return; + setRoleUpdating(user.id); + try { + await api.updateUserRole(user.id, { role }); + toast.success(`${user.username} is now ${role === 'admin' ? 'an admin' : 'a user'}.`); + onRefresh(); + } catch (err) { + toast.error(err.message || 'Failed to update user role.'); + } finally { + setRoleUpdating(null); + } + }; + + const handleActiveChange = async (user, active) => { + setActiveUpdating(user.id); + try { + await api.updateUserActive(user.id, { active }); + toast.success(`${user.username} is now ${active ? 'active' : 'inactive'}.`); + onRefresh(); + } catch (err) { + toast.error(err.message || 'Failed to update user status.'); + } finally { + setActiveUpdating(null); + } + }; + + return ( + <> + + + Users + + +
+ + + + + + + + + + + + {(users || []).map(user => { + const form = getForm(user.id); + const isSelf = currentUser?.id === user.id; + return ( + + + + + + + + + ); + })} + {!users?.length && ( + + + + )} + +
UsernameRoleStatusPasswordReset Password +
+
+ {user.username} + {user.is_default_admin && default admin} +
+
+
+ {user.role} + +
+
+ + + {user.must_change_password + ? Temporary + : Set + } + + {form.open ? ( +
+ setReset(user.id, { pw: e.target.value })} + className="h-8 text-sm w-36" + /> + + +
+ ) : ( + + )} +
+ {!isSelf && ( + + )} +
No users found.
+
+
+
+ + { if (!open) setDeleteTarget(null); }}> + + + Delete {deleteTarget?.username}? + + This is permanent in 2026. The user account and all user-owned data will be deleted, including bills, + payments, categories, monthly state, monthly starting amounts, imports, import history, and sessions. + This cannot be undone from BillTracker. + + + + Cancel + + {deleting ? 'Deleting…' : 'Delete User'} + + + + + + ); +} diff --git a/client/components/admin/adminShared.jsx b/client/components/admin/adminShared.jsx new file mode 100644 index 0000000..0c99fb2 --- /dev/null +++ b/client/components/admin/adminShared.jsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Badge } from '@/components/ui/badge'; +import { Label } from '@/components/ui/label'; + +export function SectionHeading({ children }) { + return

{children}

; +} + +export function FieldRow({ label, children }) { + return ( +
+ + {children} +
+ ); +} + +export function Toggle({ checked, onChange, label, disabled = false }) { + return ( + + ); +} + +export function formatDateTime(value) { + if (!value) return '—'; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return date.toLocaleString(); +} + +export function BackupTypeBadge({ type }) { + const cls = { + manual: 'bg-blue-500/15 text-blue-400 border-blue-500/20', + scheduled: 'bg-emerald-500/15 text-emerald-400 border-emerald-500/20', + imported: 'bg-violet-500/15 text-violet-400 border-violet-500/20', + 'pre-restore': 'bg-amber-500/15 text-amber-400 border-amber-500/20', + }[type] || 'bg-muted text-muted-foreground border-border'; + + return {type || 'backup'}; +} diff --git a/client/components/data/DownloadMyDataSection.jsx b/client/components/data/DownloadMyDataSection.jsx new file mode 100644 index 0000000..7a44904 --- /dev/null +++ b/client/components/data/DownloadMyDataSection.jsx @@ -0,0 +1,107 @@ +import React, { useState } from 'react'; +import { toast } from 'sonner'; +import { Database, Download, FileSpreadsheet, AlertTriangle, CheckCircle2, XCircle, Loader2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { SectionCard } from './dataShared'; + +const USER_EXPORTS_AVAILABLE = true; + +function ExportCard({ icon: Icon, title, description, filename, endpoint }) { + const [loading, setLoading] = useState(false); + + const handleDownload = async () => { + setLoading(true); + try { + const res = await fetch(endpoint, { credentials: 'include' }); + if (!res.ok) { + let data = {}; + try { data = await res.json(); } catch {} + throw new Error(data.error || `HTTP ${res.status}`); + } + const disposition = res.headers.get('Content-Disposition'); + const match = disposition?.match(/filename="?([^"]+)"?/i); + const name = match ? match[1] : filename; + const blob = await res.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; a.download = name; a.click(); + URL.revokeObjectURL(url); + toast.success(`${title} downloaded.`); + } catch (err) { + toast.error(err.message || 'Download failed.'); + } finally { + setLoading(false); + } + }; + + const disabled = !USER_EXPORTS_AVAILABLE || loading; + return ( +
+
+
+ +
+
+
+

{title}

+ {!USER_EXPORTS_AVAILABLE && ( + + Coming soon + + )} +
+

{description}

+
+
+
+ +
+
+ ); +} + +export default function DownloadMyDataSection() { + return ( + + + +
+ +

Exports may contain sensitive account metadata (website URLs, usernames, account info). Store exported files securely and avoid sharing them unencrypted.

+
+
+
+

What's included

+
    + {['Bills','Payments','Categories','Monthly bill state','Monthly starting amounts','History ranges','Notes','Export metadata'].map(i => ( +
  • + {i} +
  • + ))} +
+
+
+

What's not included

+
    + {['Passwords','Sessions','Admin settings','Server configuration',"Other users' data",'Full system backup files'].map(i => ( +
  • + {i} +
  • + ))} +
+
+
+
+ ); +} diff --git a/client/components/data/ImportHistorySection.jsx b/client/components/data/ImportHistorySection.jsx new file mode 100644 index 0000000..32746b8 --- /dev/null +++ b/client/components/data/ImportHistorySection.jsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { RefreshCw, Clock } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { SectionCard, fmt } from './dataShared'; + +export default function ImportHistorySection({ history, loading, onRefresh }) { + if (loading) { + return ( + +
Loading…
+
+ ); + } + + const rows = history ?? []; + + return ( + +
+

+ {rows.length === 0 ? 'No imports yet.' : `${rows.length} import${rows.length === 1 ? '' : 's'}`} +

+ +
+ {rows.length > 0 && ( +
+ + + + {['Date','File','Sheet','Parsed','Created','Updated','Skipped','Errors'].map(h => ( + + ))} + + + + {rows.map(r => ( + + + + + + + + + + + ))} + +
{h}
+ {fmt(r.imported_at)} + {r.source_filename || '—'}{r.sheet_name || '—'}{r.rows_parsed}{r.rows_created}{r.rows_updated}{r.rows_skipped}{r.rows_errored}
+
+ )} +
+ ); +} diff --git a/client/components/data/ImportMyDataSection.jsx b/client/components/data/ImportMyDataSection.jsx new file mode 100644 index 0000000..6a94d31 --- /dev/null +++ b/client/components/data/ImportMyDataSection.jsx @@ -0,0 +1,217 @@ +import React, { useState, useRef } from 'react'; +import { toast } from 'sonner'; +import { Database, Upload, AlertTriangle, Loader2 } from 'lucide-react'; +import { api } from '@/api'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { + AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, + AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { SectionCard, CountPill, fmt, importErrorState } from './dataShared'; + +export default function ImportMyDataSection({ onHistoryRefresh }) { + const fileRef = useRef(null); + const [file, setFile] = useState(null); + const [preview, setPreview] = useState({ status: 'idle', data: null, error: null }); + const [applyState, setApplyState] = useState({ status: 'idle', result: null, error: null }); + const [confirmOpen, setConfirmOpen] = useState(false); + + const reset = () => { + setFile(null); + setPreview({ status: 'idle', data: null, error: null }); + setApplyState({ status: 'idle', result: null, error: null }); + if (fileRef.current) fileRef.current.value = ''; + }; + + const handlePreview = async () => { + if (!file) { + toast.error('Choose a SQLite data export first.'); + return; + } + setPreview({ status: 'loading', data: null, error: null }); + setApplyState({ status: 'idle', result: null, error: null }); + try { + const data = await api.previewUserDbImport(file); + setPreview({ status: 'ready', data, error: null }); + toast.success('SQLite export preview ready.'); + } catch (err) { + setPreview({ status: 'error', data: null, error: importErrorState(err, 'SQLite import preview failed.') }); + toast.error(err.message || 'SQLite import preview failed.'); + } + }; + + const handleApply = () => { + if (!preview.data?.import_session_id) return; + setConfirmOpen(true); + }; + + const handleConfirmImport = async () => { + setConfirmOpen(false); + setApplyState({ status: 'loading', result: null, error: null }); + try { + const result = await api.applyUserDbImport({ + import_session_id: preview.data.import_session_id, + options: { overwrite: false }, + }); + setApplyState({ status: 'done', result, error: null }); + toast.success('SQLite data import applied.'); + onHistoryRefresh?.(); + } catch (err) { + setApplyState({ status: 'error', result: null, error: importErrorState(err, 'SQLite import apply failed.') }); + toast.error(err.message || 'SQLite import apply failed.'); + } + }; + + const counts = preview.data?.counts || {}; + const summary = preview.data?.summary || {}; + + return ( + <> + +
+
+
+ +
+

Import a SQLite data export created by this app.

+

+ This is not a full system restore. Existing records are skipped by default, and admin/system data is never imported. +

+
+
+
+ +
+ +
+ + +
+
+ + {preview.status === 'error' && ( +
+ + {preview.error?.message || 'SQLite import preview failed.'} + {preview.error?.details?.length > 0 && ( +
    + {preview.error.details.map((d, i) => ( +
  • {d.message || d.table || JSON.stringify(d)}
  • + ))} +
+ )} +
+ )} + + {preview.status === 'ready' && preview.data && ( +
+
+
+
+

Preview ready

+

+ Exported {fmt(preview.data.metadata?.exported_at)} · {preview.data.source_filename || 'SQLite export'} +

+
+ + User data only + +
+
+ + + + + +
+
+ {Object.entries(summary).filter(([, v]) => v && typeof v === 'object').map(([key, value]) => ( +
+

{key.replace(/_/g, ' ')}

+

+ create {value.create || 0} · skip {value.skip || 0} · conflict {value.conflict || 0} +

+
+ ))} +
+ {preview.data.warnings?.length > 0 && ( +
+ {preview.data.warnings.map((warning, i) => ( +

+ {warning} +

+ ))} +
+ )} +
+ +
+

Review the preview before applying. Nothing is imported until you confirm.

+ +
+
+ )} + + {applyState.status === 'done' && applyState.result && ( +
+

SQLite import applied

+
+ + + + +
+
+ )} + + {applyState.status === 'error' && ( +
+ {applyState.error?.message || 'SQLite import apply failed.'} +
+ )} +
+
+ {/* Import confirmation dialog */} + + + + Import SQLite data export? + + Import this SQLite data export into your account? Existing records will be skipped by default. + + + + Cancel + + Confirm Import + + + + + + ); +} diff --git a/client/components/data/ImportSpreadsheetSection.jsx b/client/components/data/ImportSpreadsheetSection.jsx new file mode 100644 index 0000000..3e4038c --- /dev/null +++ b/client/components/data/ImportSpreadsheetSection.jsx @@ -0,0 +1,1435 @@ +import React, { useState, useEffect, useRef, useMemo } from 'react'; +import { toast } from 'sonner'; +import { + Upload, FileSpreadsheet, AlertTriangle, CheckCircle2, CheckCheck, + Loader2, RefreshCw, ChevronDown, ChevronUp, SkipForward, Plus, + List, Building2, ChevronLeft, FileText, XCircle, Sparkles, +} from 'lucide-react'; +import { api } from '@/api'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Switch } from '@/components/ui/switch'; +import { + AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, + AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog'; +import { SectionCard, CountPill, fmt, importErrorState } from './dataShared'; + +function groupRowsBySheet(rows) { + const map = new Map(); + for (const row of rows) { + const key = row.sheet_name || '(unknown sheet)'; + if (!map.has(key)) map.set(key, []); + map.get(key).push(row); + } + return Array.from(map.entries()).map(([name, rows]) => ({ name, rows })); +} + +function initialDecisionFromRecommendation(row) { + const rec = row.recommendation || {}; + const action = rec.action === 'ambiguous' ? null : (rec.action || row.proposed_action || null); + + if (!action || row.requires_user_decision) return { action: null }; + if (action === 'skip_row') return { action: 'skip_row' }; + if (action === 'match_existing_bill') { + return { + action, + bill_id: rec.bill_id ?? row.possible_bill_matches?.[0]?.bill_id ?? null, + bill_name: null, + due_day: rec.due_day ?? null, + actual_amount: rec.actual_amount ?? row.detected_amount ?? null, + payment_amount: rec.payment_amount ?? row.detected_payment_amount ?? null, + payment_date: rec.payment_date ?? row.detected_paid_date ?? null, + notes: row.detected_notes ?? null, + }; + } + if (action === 'create_new_bill') { + return { + action, + bill_id: null, + bill_name: rec.bill_name || row.detected_bill_name || '', + category_id: rec.category_id ?? null, + due_day: rec.due_day ?? null, + expected_amount: rec.expected_amount ?? row.detected_amount ?? null, + actual_amount: rec.actual_amount ?? row.detected_amount ?? null, + payment_amount: rec.payment_amount ?? row.detected_payment_amount ?? null, + payment_date: rec.payment_date ?? row.detected_paid_date ?? null, + notes: row.detected_notes ?? null, + }; + } + return { action }; +} + +function safeRawBillName(row) { + const raw = row.raw_values?.find((v) => { + const text = String(v || '').trim(); + if (!text || text.length > 80) return false; + if (/^(?:total|subtotal|sum|grand\s*total)$/i.test(text)) return false; + if (/^\$?\(?\d[\d,]*(?:\.\d{1,2})?\)?$/.test(text)) return false; + if (/^\d{1,2}\/\d{1,2}(?:\/\d{2,4})?$/.test(text)) return false; + if (/^\d{4}-\d{2}-\d{2}$/.test(text)) return false; + return true; + }); + return raw ? String(raw).trim() : ''; +} + +function buildCreateNewDecision(row, currentDecision = {}) { + const rec = row.recommendation || {}; + const billName = currentDecision.bill_name + || row.detected_bill_name + || rec.bill_name + || safeRawBillName(row); + + return { + ...currentDecision, + action: 'create_new_bill', + previous_match_bill_id: currentDecision.bill_id ?? currentDecision.previous_match_bill_id ?? rec.bill_id ?? row.possible_bill_matches?.[0]?.bill_id ?? null, + bill_id: null, + bill_name: billName, + category_id: currentDecision.category_id ?? rec.category_id ?? null, + due_day: currentDecision.due_day ?? rec.due_day ?? null, + expected_amount: currentDecision.expected_amount ?? rec.expected_amount ?? row.detected_amount ?? null, + actual_amount: currentDecision.actual_amount ?? rec.actual_amount ?? row.detected_amount ?? null, + payment_amount: currentDecision.payment_amount ?? rec.payment_amount ?? row.detected_payment_amount ?? null, + payment_date: currentDecision.payment_date ?? rec.payment_date ?? row.detected_paid_date ?? null, + notes: currentDecision.notes ?? row.detected_notes ?? null, + }; +} + +function buildInitialDecisions(rows) { + const d = {}; + for (const row of rows) { + const hasError = row.errors?.length > 0; + if (hasError || row.proposed_action === 'skip_row') { + d[row.row_id] = { action: 'skip_row' }; + } else { + d[row.row_id] = initialDecisionFromRecommendation(row); + } + } + return d; +} + +function isDecisionComplete(action, decision) { + if (!action) return false; + if (action === 'skip_row') return true; + if (action === 'create_new_bill') return !!(decision?.bill_name?.trim()); + if (['match_existing_bill', 'update_monthly_state', 'add_monthly_note', 'create_payment'].includes(action)) { + return !!decision?.bill_id; + } + return true; +} + +// ─── Badges ─────────────────────────────────────────────────────────────────── + +function SourceBadge({ source }) { + const MAP = { + row_date: 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400', + sheet_name: 'bg-blue-500/15 text-blue-600 dark:text-blue-400', + default: 'bg-amber-500/15 text-amber-600 dark:text-amber-500', + ambiguous: 'bg-red-500/15 text-red-600 dark:text-red-400', + }; + const LABELS = { row_date: 'date cell', sheet_name: 'tab name', default: 'default', ambiguous: 'ambiguous' }; + return ( + + {LABELS[source] ?? source} + + ); +} + +function ConfidenceBadge({ confidence }) { + const MAP = { high: 'text-emerald-600 dark:text-emerald-400', medium: 'text-amber-600 dark:text-amber-500', low: 'text-red-500' }; + return {confidence}; +} + +function actionLabel(action) { + const MAP = { + match_existing_bill: 'Match existing bill', + create_new_bill: 'Create new bill', + skip_row: 'Skip row', + ambiguous: 'Needs decision', + update_monthly_state: 'Update monthly record', + add_monthly_note: 'Add monthly note', + create_payment: 'Record as payment', + }; + return MAP[action] || (action ? action.replace(/_/g, ' ') : 'Needs decision'); +} + + +function SheetStatusBadge({ status }) { + const MAP = { + parsed: 'bg-emerald-500/15 text-emerald-600', + parsed_month_only: 'bg-amber-500/15 text-amber-600', + ambiguous: 'bg-orange-500/15 text-orange-600', + skipped: 'bg-muted text-muted-foreground', + }; + const LABELS = { + parsed: 'parsed', parsed_month_only: 'month only', ambiguous: 'ambiguous', skipped: 'skipped', + }; + return ( + + {LABELS[status] ?? status} + + ); +} +function WorkbookSummaryCard({ workbook }) { + const isMulti = workbook.parse_mode === 'all_sheets'; + + return ( +
+
+

Workbook Summary

+ + {isMulti ? `${workbook.total_row_count} rows across ${workbook.sheet_names.length} tabs` : `${workbook.row_count} rows · ${workbook.selected_sheet}`} + +
+ {isMulti && workbook.sheets?.length > 0 && ( +
+ {workbook.sheets.map(s => ( +
+ {s.name} +
+ {s.detected_year && s.detected_month && ( + + {String(s.detected_month).padStart(2,'0')}/{s.detected_year} + + )} + + {s.status !== 'skipped' && {s.row_count} rows} +
+
+ ))} +
+ )} +
+ ); +} + +// ─── XLSX Import: Row Decision Controls ────────────────────────────────────── + +const ACTIONS_NEEDING_BILL = new Set(['match_existing_bill','update_monthly_state','add_monthly_note','create_payment']); + +function RowDecisionRow({ row, decision, onDecisionChange, allBills, categories, selected, onSelectedChange }) { + const [expanded, setExpanded] = useState(row.requires_user_decision || !decision?.action); + + const action = decision?.action ?? null; + const isSkip = action === 'skip_row'; + const hasError = row.errors?.length > 0; + const complete = isDecisionComplete(action, decision); + const rec = row.recommendation || {}; + + const suggestedBills = row.possible_bill_matches ?? []; + const suggestedIds = new Set(suggestedBills.map(b => b.bill_id)); + const otherBills = allBills.filter(b => !suggestedIds.has(b.id)); + + const handleAction = (val) => { + const next = { ...decision, action: val }; + if (val === 'create_new_bill') { + Object.assign(next, buildCreateNewDecision(row, decision)); + } else if (ACTIONS_NEEDING_BILL.has(val)) { + next.bill_name = null; + next.bill_id = decision?.bill_id ?? decision?.previous_match_bill_id ?? rec.bill_id ?? suggestedBills[0]?.bill_id ?? null; + next.actual_amount = decision?.actual_amount ?? rec.actual_amount ?? row.detected_amount ?? null; + next.payment_amount = decision?.payment_amount ?? rec.payment_amount ?? row.detected_payment_amount ?? null; + next.payment_date = decision?.payment_date ?? rec.payment_date ?? row.detected_paid_date ?? null; + } else { + next.bill_id = null; + next.bill_name = null; + } + onDecisionChange(row.row_id, next); + if (val === 'skip_row') setExpanded(false); + }; + + const handleBill = (e) => { + onDecisionChange(row.row_id, { ...decision, bill_id: parseInt(e.target.value, 10) || null }); + }; + + const handleBillName = (e) => { + onDecisionChange(row.row_id, { ...decision, bill_name: e.target.value }); + }; + + const handleDecisionField = (field, value) => { + onDecisionChange(row.row_id, { ...decision, [field]: value }); + }; + + return ( +
+ {/* Main row */} +
setExpanded(e => !e)} + > + {/* Selection */} +
e.stopPropagation()}> + onSelectedChange(row.row_id, e.target.checked)} + aria-label={`Select row ${row.source_row_number}`} + className="h-4 w-4 rounded border-border accent-primary" + /> +
+ + {/* Status icon */} +
+ {hasError ? : + isSkip ? : + complete ? : + action !== null ? : + } +
+ + {/* Content */} +
+
+ #{row.source_row_number} + {row.sheet_name && {row.sheet_name}} + {row.detected_year && row.detected_month && ( + + {String(row.detected_month).padStart(2,'0')}/{row.detected_year} + + )} + {row.year_month_source && } +
+
+ + {row.detected_bill_name || '(no bill name)'} + + {row.detected_amount != null && ( + + ${row.detected_amount.toFixed(2)} + + )} + {row.detected_paid_date && ( + + paid {row.detected_paid_date} + + )} + {row.detected_labels?.length > 0 && ( + {row.detected_labels.join(', ')} + )} + {row.detected_notes && ( + {row.detected_notes} + )} +
+
+ + {/* Right: action status + expand */} +
+ {action === null ? ( + Needs decision + ) : isSkip ? ( + Skipped + ) : ( + {action.replace(/_/g,' ')} + )} + {action !== 'skip_row' && ( + expanded ? : + )} +
+
+ + {/* Expanded decision controls */} + {expanded && !hasError && ( +
+ {/* Recommendation */} + {rec.action && ( +
+
+ Recommended: {actionLabel(rec.action)} + {rec.bill_name && rec.action === 'match_existing_bill' && ( + → {rec.bill_name} + )} + {rec.category_name && ( + Category: {rec.category_name} + )} + {rec.due_day && Due day: {rec.due_day}} + {rec.actual_amount != null && ${Number(rec.actual_amount).toFixed(2)}} + +
+ {rec.reason &&

Reason: {rec.reason}

} +
+ )} + + {/* Warnings */} + {(rec.warnings?.length > 0 || row.warnings?.length > 0) && ( +
+ {Array.from(new Set([...(rec.warnings || []), ...(row.warnings || [])])).map((w, i) => ( +

+ {w} +

+ ))} +
+ )} + + {/* Possible matches hint */} + {suggestedBills.length > 0 && ( +
+ Suggested: + {suggestedBills.slice(0, 3).map(b => ( + + ))} +
+ )} + + {/* Action selector */} +
+ + +
+ + {/* Bill selector (for actions that need a bill) */} + {ACTIONS_NEEDING_BILL.has(action) && ( +
+ + +
+ )} + + {/* Bill name input for create_new_bill */} + {action === 'create_new_bill' && ( +
+
+ + +
+
+ + + {rec.category_name && Suggested: {rec.category_name}} +
+
+ + handleDecisionField('due_day', e.target.value ? parseInt(e.target.value, 10) : null)} + placeholder="Due day" + className="h-8 text-sm w-24" + /> + handleDecisionField('expected_amount', e.target.value === '' ? null : parseFloat(e.target.value))} + placeholder="Expected amount" + className="h-8 text-sm w-40" + /> +
+
+ )} + + {action && action !== 'skip_row' && ( +
+ + handleDecisionField('payment_date', e.target.value || null)} + className="h-8 text-sm w-40" + /> + handleDecisionField('payment_amount', e.target.value === '' ? null : parseFloat(e.target.value))} + placeholder="Paid amount" + className="h-8 text-sm w-36" + /> +
+ )} + + {/* Quick skip */} + {action !== 'skip_row' && ( + + )} +
+ )} +
+ ); +} + +// ─── XLSX Import: Preview Table ─────────────────────────────────────────────── + +function PreviewTable({ rows, decisions, onDecisionChange, allBills, categories, selectedRows, onSelectedChange }) { + const groups = groupRowsBySheet(rows); + const multiTab = groups.length > 1; + + return ( +
+ {groups.map(({ name, rows: groupRows }) => ( +
+ {multiTab && ( +
+ + {name} + · {groupRows.length} rows +
+ )} + {groupRows.map(row => ( + + ))} +
+ ))} +
+ ); +} + +function BulkActionBar({ + rows, + selectedRows, + onSelectAll, + onClearSelection, + onBulkSkip, + onBulkCreateNew, + onBulkReset, +}) { + const allSelected = rows.length > 0 && rows.every(r => selectedRows.has(r.row_id)); + const selectedCount = selectedRows.size; + + return ( +
+
+ + +
+ {selectedCount > 0 && ( + {selectedCount} row{selectedCount === 1 ? '' : 's'} selected + )} + {selectedCount > 0 && ( + <> + + + + + + )} +
+
+
+ ); +} + +// ─── Section 1: Import Spreadsheet History ──────────────────────────────────── + +const INITIAL_OPTIONS = { + parseAllSheets: true, + defaultYear: new Date().getFullYear(), + defaultMonth: '', +}; + +// ─── Bill History Import helpers ────────────────────────────────────────────── + +function ConfidenceDot({ level }) { + const cls = level === 'high' ? 'bg-emerald-500' + : level === 'medium' ? 'bg-amber-500' + : 'bg-muted-foreground/30'; + return ; +} + +function useBillGroups(previewRows, allBills) { + return useMemo(() => { + const billMap = new Map(allBills.map(b => [b.id, b])); + const groups = new Map(); + + for (const row of previewRows) { + for (const match of (row.possible_bill_matches ?? [])) { + if (!billMap.has(match.bill_id)) continue; + if (!groups.has(match.bill_id)) { + groups.set(match.bill_id, { + bill: billMap.get(match.bill_id), + rows: [], + counts: { high: 0, medium: 0, low: 0 }, + }); + } + const g = groups.get(match.bill_id); + if (!g.rows.find(r => r.row_id === row.row_id)) { + g.rows.push({ ...row, _match: match }); + g.counts[match.match_confidence] = (g.counts[match.match_confidence] || 0) + 1; + } + } + } + return [...groups.values()].sort((a, b) => + b.rows.length !== a.rows.length ? b.rows.length - a.rows.length : b.counts.high - a.counts.high + ); + }, [previewRows, allBills]); +} + +function rowDateLabel(row) { + if (row.detected_year && row.detected_month) + return `${row.detected_year}-${String(row.detected_month).padStart(2, '0')}`; + return row.detected_paid_date ?? '—'; +} + +function billImportProgress(rows, importResult) { + const completedRowIds = importResult?.completedRowIds ?? new Set(); + const remainingRows = rows.filter(row => !completedRowIds.has(row.row_id)); + return { + completedCount: rows.length - remainingRows.length, + remainingRows, + remainingCount: remainingRows.length, + }; +} + +function detailImportedAnything(detail) { + return ['created', 'updated', 'overwritten', 'imported'].includes(detail?.result) + || detail?.payment === 'created'; +} + +function detailCompletesImport(detail) { + if (!detail?.row_id) return false; + if (['error', 'ambiguous', 'skipped_conflict'].includes(detail.result)) return false; + if (detail.result === 'skipped') return false; + return detailImportedAnything(detail) + || detail.result === 'skipped_duplicate' + || detail.payment === 'skipped_duplicate'; +} + +function BillDetailView({ group, onBack, onImport, isImporting, importResult }) { + const { bill, rows } = group; + const { completedCount, remainingCount } = billImportProgress(rows, importResult); + const sorted = [...rows].sort((a, b) => { + const da = (a.detected_year ?? 0) * 100 + (a.detected_month ?? 0); + const db = (b.detected_year ?? 0) * 100 + (b.detected_month ?? 0); + return da - db; + }); + + return ( +
+
+ + {bill.name} +
+ {importResult && ( +
+ + {completedCount === rows.length + ? 'All imported' + : `${completedCount} imported · ${remainingCount} remaining`} + {importResult.duplicates > 0 + && ` · ${importResult.duplicates} dupes`} + + {importResult.duplicates > 0 && importResult.earliestDup && ( +

+ {importResult.duplicates} dup{importResult.duplicates > 1 ? 's' : ''} · recorded{' '} + {importResult.earliestDup.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })} +

+ )} +
+ )} + +
+
+
+ {sorted.map(row => ( +
+ + {rowDateLabel(row)} + + {row.detected_amount != null ? `$${Number(row.detected_amount).toFixed(2)}` : '—'} + + {row.detected_name ?? '—'} + + {row._match.match_confidence} + +
+ ))} +
+
+ ); +} + +function BillHistoryView({ previewRows, allBills, importingBillId, billImportResults, onImportBill }) { + const [selectedBillId, setSelectedBillId] = useState(null); + const billGroups = useBillGroups(previewRows, allBills); + + if (billGroups.length === 0) { + return ( +
+ No existing bills matched rows in this file. +
+ ); + } + + if (selectedBillId) { + const group = billGroups.find(g => g.bill.id === selectedBillId); + return group + ? setSelectedBillId(null)} + onImport={() => onImportBill(group)} /> + : null; + } + + return ( +
+ {billGroups.map(g => { + const { bill, rows, counts } = g; + const isImporting = importingBillId === bill.id; + const importResult = billImportResults.get(bill.id) ?? null; + const { completedCount, remainingCount } = billImportProgress(rows, importResult); + + const sorted3 = [...rows] + .sort((a, b) => { + const da = (a.detected_year ?? 0) * 100 + (a.detected_month ?? 0); + const db = (b.detected_year ?? 0) * 100 + (b.detected_month ?? 0); + return da - db; + }) + .slice(0, 3); + + return ( +
+
+
+ {bill.name} + + {rows.length} row{rows.length !== 1 ? 's' : ''} + + {counts.high > 0 && {counts.high} high} + {counts.medium > 0 && {counts.medium} med} + {counts.low > 0 && {counts.low} low} + {importResult && (() => { + const allImported = completedCount === rows.length; + const fmtDate = d => d?.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); + const dupDate = importResult.earliestDup ? ` · ${fmtDate(importResult.earliestDup)}` : ''; + return ( +
+ + {allImported ? 'All imported' : `${completedCount} imported · ${remainingCount} remaining`} + {importResult.duplicates > 0 && ` · ${importResult.duplicates} dupes`} + {importResult.errored > 0 && ` · ${importResult.errored} errors`} + + {importResult.duplicates > 0 && ( +

+ {importResult.duplicates} duplicate{importResult.duplicates > 1 ? 's' : ''}{dupDate} +

+ )} +
+ ); + })()} +
+
+ {sorted3.map(row => ( +
+ + {rowDateLabel(row)} + {row.detected_amount != null && ( + ${Number(row.detected_amount).toFixed(2)} + )} + {row.detected_name && + row.detected_name.toLowerCase() !== bill.name.toLowerCase() && ( + "{row.detected_name}" + )} +
+ ))} + {rows.length > 3 && ( + + )} +
+
+
+ {importResult ? ( + + ) : ( + + )} + +
+
+ ); + })} +
+ ); +} + +// ───────────────────────────────────────────────────────────────────────────── + +export default function ImportSpreadsheetSection({ onHistoryRefresh }) { + const fileRef = useRef(null); + const [file, setFile] = useState(null); + const [options, setOptions] = useState(INITIAL_OPTIONS); + const [preview, setPreview] = useState({ status: 'idle', data: null, error: null }); + const [decisions, setDecisions] = useState({}); + const [applyState, setApplyState] = useState({ status: 'idle', result: null, error: null }); + const [allBills, setAllBills] = useState([]); + const [categories, setCategories] = useState([]); + const [selectedRows, setSelectedRows] = useState(new Set()); + const [viewMode, setViewMode] = useState('rows'); // 'rows' | 'bills' + const [importingBillId, setImportingBillId] = useState(null); + const [billImportResults, setBillImportResults] = useState(new Map()); // bill_id → { created, updated, errored } + + // Load bills/categories for the decision controls + useEffect(() => { + api.bills().then(setAllBills).catch(() => {}); + api.categories().then(setCategories).catch(() => {}); + }, []); + + const opt = (k, v) => setOptions(prev => ({ ...prev, [k]: v })); + + // ── Preview ────────────────────────────────────────────────────────────────── + const handlePreview = async () => { + if (!file) return; + setPreview({ status: 'loading', data: null, error: null }); + setDecisions({}); + setSelectedRows(new Set()); + setApplyState({ status: 'idle', result: null, error: null }); + setViewMode('rows'); + setImportingBillId(null); + setBillImportResults(new Map()); + try { + const data = await api.previewSpreadsheetImport(file, { + parseAllSheets: options.parseAllSheets, + defaultYear: options.defaultYear ? parseInt(options.defaultYear, 10) : null, + defaultMonth: options.defaultMonth ? parseInt(options.defaultMonth, 10) : null, + }); + setPreview({ status: 'ready', data, error: null }); + setDecisions(buildInitialDecisions(data.rows)); + } catch (err) { + setPreview({ status: 'error', data: null, error: importErrorState(err, 'Preview failed.') }); + } + }; + + // ── Decision update ────────────────────────────────────────────────────────── + const handleDecisionChange = (rowId, decision) => { + setDecisions(prev => ({ ...prev, [rowId]: decision })); + }; + + const handleSelectedChange = (rowId, selected) => { + setSelectedRows(prev => { + const next = new Set(prev); + if (selected) next.add(rowId); + else next.delete(rowId); + return next; + }); + }; + + const clearSelection = () => setSelectedRows(new Set()); + + // ── Bill-history direct import ──────────────────────────────────────────── + // Applies all matching rows for a bill immediately — no queue, no review step. + const handleDirectImportBill = async (group) => { + const sessionId = preview.data?.import_session_id; + if (!sessionId || importingBillId) return; + + const previousResult = billImportResults.get(group.bill.id) ?? null; + const rowsToImport = billImportProgress(group.rows, previousResult).remainingRows; + if (rowsToImport.length === 0) { + toast.info(`All rows for "${group.bill.name}" have already been imported.`); + return; + } + + setImportingBillId(group.bill.id); + try { + const decisionsList = rowsToImport.map(row => ({ + row_id: row.row_id, + action: 'match_existing_bill', + bill_id: group.bill.id, + actual_amount: row.detected_amount ?? null, + payment_amount: row.detected_payment_amount ?? row.detected_amount ?? null, + payment_date: row.detected_paid_date ?? null, + })); + + const result = await api.applySpreadsheetImport({ + import_session_id: sessionId, + decisions: decisionsList, + options: {}, + }); + + const created = result.rows_created ?? 0; + const updated = result.rows_updated ?? 0; + const errored = result.rows_errored ?? 0; + const details = result.details ?? []; + const duplicateRowIds = new Set( + details + .filter(d => d.result === 'skipped_duplicate' || d.payment === 'skipped_duplicate') + .map(d => d.row_id) + .filter(Boolean), + ); + const duplicates = duplicateRowIds.size || (result.rows_duplicates ?? 0); + + // Collect created_at dates from duplicate detail entries so we can show + // when the existing payments were originally recorded. + const dupDates = details + .filter(d => (d.result === 'skipped_duplicate' || d.payment === 'skipped_duplicate') && d.existing_created_at) + .map(d => new Date(d.existing_created_at)) + .filter(d => !isNaN(d.getTime())) + .sort((a, b) => a - b); + + const earliestDup = dupDates[0] ?? null; + const latestDup = dupDates.at(-1) ?? null; + const completedRowIds = new Set(previousResult?.completedRowIds ?? []); + const erroredRowIds = new Set(previousResult?.erroredRowIds ?? []); + + for (const detail of details) { + if (detailCompletesImport(detail)) { + completedRowIds.add(detail.row_id); + erroredRowIds.delete(detail.row_id); + } else if (['error', 'ambiguous', 'skipped_conflict'].includes(detail?.result)) { + erroredRowIds.add(detail.row_id); + } + } + + const mergedResult = { + created: (previousResult?.created ?? 0) + created, + updated: (previousResult?.updated ?? 0) + updated, + errored: erroredRowIds.size, + duplicates: (previousResult?.duplicates ?? 0) + duplicates, + earliestDup: previousResult?.earliestDup && earliestDup + ? (previousResult.earliestDup < earliestDup ? previousResult.earliestDup : earliestDup) + : (previousResult?.earliestDup ?? earliestDup), + latestDup: previousResult?.latestDup && latestDup + ? (previousResult.latestDup > latestDup ? previousResult.latestDup : latestDup) + : (previousResult?.latestDup ?? latestDup), + completedRowIds, + erroredRowIds, + }; + + setBillImportResults(prev => new Map(prev).set(group.bill.id, mergedResult)); + + const imported = created + updated; + const fmtDate = d => d?.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); + const remainingCount = billImportProgress(group.rows, mergedResult).remainingCount; + + if (imported === 0 && duplicates > 0) { + const dateHint = earliestDup + ? ` (first recorded ${fmtDate(earliestDup)})` + : ''; + toast.warning( + remainingCount === 0 + ? `All rows for "${group.bill.name}" are now imported${dateHint}.` + : `${duplicates} row${duplicates > 1 ? 's' : ''} for "${group.bill.name}" already exist${dateHint}. ${remainingCount} remaining.`, + ); + } else { + const parts = [`${imported} entr${imported === 1 ? 'y' : 'ies'} imported`]; + if (duplicates > 0) { + const dateHint = earliestDup ? ` (recorded ${fmtDate(earliestDup)})` : ''; + parts.push(`${duplicates} already existed${dateHint}`); + } + if (errored > 0) parts.push(`${errored} error${errored > 1 ? 's' : ''}`); + if (remainingCount > 0) parts.push(`${remainingCount} remaining`); + toast.success(`${group.bill.name} — ${parts.join(' · ')}`); + } + onHistoryRefresh?.(); + } catch (err) { + toast.error(err.message || `Import failed for "${group.bill.name}"`); + } finally { + setImportingBillId(null); + } + }; + + const selectAllVisibleRows = () => { + setSelectedRows(new Set((preview.data?.rows || []).map(r => r.row_id))); + }; + + const selectedPreviewRows = () => { + const selected = selectedRows; + return (preview.data?.rows || []).filter(r => selected.has(r.row_id)); + }; + + const handleBulkSkip = () => { + const rows = selectedPreviewRows(); + setDecisions(prev => { + const next = { ...prev }; + rows.forEach(row => { + next[row.row_id] = { ...(next[row.row_id] || {}), action: 'skip_row', bill_id: null, bill_name: null }; + }); + return next; + }); + toast.success(`${rows.length} row${rows.length === 1 ? '' : 's'} marked skipped.`); + }; + + const handleBulkCreateNew = () => { + const rows = selectedPreviewRows(); + let missingNames = 0; + setDecisions(prev => { + const next = { ...prev }; + rows.forEach(row => { + const decision = buildCreateNewDecision(row, next[row.row_id] || {}); + if (!decision.bill_name?.trim()) missingNames++; + next[row.row_id] = decision; + }); + return next; + }); + if (missingNames > 0) { + toast.warning(`${missingNames} selected row${missingNames === 1 ? '' : 's'} still need a bill name.`); + } else { + toast.success(`${rows.length} row${rows.length === 1 ? '' : 's'} set to create new bills.`); + } + }; + + const handleBulkReset = () => { + const rows = selectedPreviewRows(); + setDecisions(prev => { + const next = { ...prev }; + rows.forEach(row => { + next[row.row_id] = initialDecisionFromRecommendation(row); + }); + return next; + }); + toast.success(`${rows.length} row${rows.length === 1 ? '' : 's'} reset to recommendation.`); + }; + + const buildApplyDecision = (row, d) => { + if (!d?.action) return null; + + const base = { + row_id: row.row_id, + action: d.action, + actual_amount: d.actual_amount ?? row.detected_amount ?? undefined, + year: row.detected_year ?? undefined, + month: row.detected_month ?? undefined, + notes: d.notes ?? row.detected_notes ?? undefined, + payment_amount: d.payment_amount ?? row.detected_payment_amount ?? undefined, + payment_date: d.payment_date ?? row.detected_paid_date ?? undefined, + }; + + if (d.action === 'create_new_bill') { + return { + ...base, + bill_name: d.bill_name?.trim() || undefined, + category_id: d.category_id ?? undefined, + due_day: d.due_day ?? undefined, + expected_amount: d.expected_amount ?? undefined, + }; + } + + if (ACTIONS_NEEDING_BILL.has(d.action)) { + return { + ...base, + bill_id: d.bill_id ?? undefined, + }; + } + + return base; + }; + + // ── Apply ──────────────────────────────────────────────────────────────────── + const handleApply = async () => { + if (!preview.data) return; + setApplyState({ status: 'loading', result: null, error: null }); + try { + const decisionsList = preview.data.rows + .map(row => { + const d = decisions[row.row_id]; + if (d?.action === 'skip_row') return null; + return buildApplyDecision(row, d); + }) + .filter(Boolean); + + if (decisionsList.length === 0) { + throw new Error('No rows are selected to import. Choose at least one row to match or create, or keep the preview for review.'); + } + + const result = await api.applySpreadsheetImport({ + import_session_id: preview.data.import_session_id, + decisions: decisionsList, + options: { reviewed_skipped_count: skipRows.length }, + }); + setApplyState({ status: 'done', result, error: null }); + setSelectedRows(new Set()); + toast.success(`Import applied — ${result.rows_created} created, ${result.rows_updated} updated.`); + onHistoryRefresh(); + } catch (err) { + const errorState = importErrorState(err, 'Apply failed.'); + setApplyState({ status: 'error', result: null, error: errorState }); + toast.error(errorState.message || 'Apply failed.'); + } + }; + + // ── Reset ──────────────────────────────────────────────────────────────────── + const handleReset = () => { + setFile(null); + setOptions(INITIAL_OPTIONS); + setPreview({ status: 'idle', data: null, error: null }); + setDecisions({}); + setSelectedRows(new Set()); + setApplyState({ status: 'idle', result: null, error: null }); + setViewMode('rows'); + setImportingBillId(null); + setBillImportResults(new Map()); + if (fileRef.current) fileRef.current.value = ''; + }; + + // ── Derived state ──────────────────────────────────────────────────────────── + const previewRows = preview.data?.rows ?? []; + const unresolvedRows = previewRows.filter(r => { + const d = decisions[r.row_id]; + return !d?.action || !isDecisionComplete(d.action, d); + }); + const pendingRows = previewRows.filter(r => decisions[r.row_id]?.action && decisions[r.row_id]?.action !== 'skip_row'); + const skipRows = previewRows.filter(r => decisions[r.row_id]?.action === 'skip_row'); + const canApply = unresolvedRows.length === 0 && pendingRows.length > 0 && applyState.status !== 'loading'; + + // ── Render ──────────────────────────────────────────────────────────────────── + return ( + + + {/* ── Upload panel ──────────────────────────────────────────────────────── */} +
+ + {/* File picker */} +
+ +
+ setFile(e.target.files?.[0] ?? null)} /> + + {file && ( + + {file.name} + + )} +
+
+ + {/* Options */} +
+
+ opt('parseAllSheets', v)} + id="parse-all" /> + +
+
+ + opt('defaultYear', e.target.value)} + className="w-24 h-8 text-sm" /> +
+ {!options.parseAllSheets && ( +
+ + opt('defaultMonth', e.target.value)} + className="w-20 h-8 text-sm" /> +
+ )} +
+ + {/* Preview button */} +
+ + {(preview.status === 'ready' || preview.status === 'error' || applyState.status !== 'idle') && ( + + )} +
+ + {/* Error from preview */} + {preview.status === 'error' && ( +
+ {preview.error?.message || preview.error || 'Preview failed.'} + {preview.error?.details?.length > 0 && ( +
    + {preview.error.details.map((d, i) => ( +
  • {d.row_id ? `${d.row_id}: ` : ''}{d.message}
  • + ))} +
+ )} +
+ )} +
+ + {/* ── Preview results ────────────────────────────────────────────────────── */} + {preview.status === 'ready' && preview.data && !['loading', 'done'].includes(applyState.status) && ( +
+ + {/* Workbook summary */} + + + {/* Row decision table */} + {previewRows.length > 0 ? ( +
+ {/* Tab header */} +
+
+ + +
+ + {viewMode === 'rows' + ? 'Select rows, apply bulk decisions, then import.' + : 'Click a bill to queue its entire history from this file.'} + +
+ + {/* Rows view */} + {viewMode === 'rows' && ( + <> + + + + )} + + {/* Bills view */} + {viewMode === 'bills' && ( + + )} +
+ ) : ( +

No data rows found in this file.

+ )} + + {/* Apply bar */} + {previewRows.length > 0 && ( +
+
+ {previewRows.length} rows reviewed + {pendingRows.length} to apply + {skipRows.length} skipped + {unresolvedRows.length > 0 && ( + {unresolvedRows.length} need a decision + )} +
+ +
+ )} +
+ )} + + {/* ── Applying ──────────────────────────────────────────────────────────── */} + {applyState.status === 'loading' && ( +
+ + Applying import… +
+ )} + + {/* ── Apply result ──────────────────────────────────────────────────────── */} + {applyState.status === 'done' && applyState.result && ( +
+
+
+ +

Import applied successfully

+
+
+ {[ + { label: 'Created', value: applyState.result.rows_created, color: 'text-emerald-600' }, + { label: 'Updated', value: applyState.result.rows_updated, color: 'text-blue-600' }, + { label: 'Skipped', value: applyState.result.rows_skipped, color: 'text-muted-foreground' }, + { label: 'Errors', value: applyState.result.rows_errored, color: 'text-red-500' }, + ].map(({ label, value, color }) => ( +
+

{value}

+

{label}

+
+ ))} +
+
+ +
+ )} + + {/* ── Apply error ───────────────────────────────────────────────────────── */} + {applyState.status === 'error' && ( +
+
+ {applyState.error?.message || applyState.error || 'Apply failed.'} + {applyState.error?.details?.length > 0 && ( +
    + {applyState.error.details.map((d, i) => ( +
  • + {d.row_id ? `${d.row_id}: ` : ''} + {d.field ? `${d.field} - ` : ''} + {d.message} +
  • + ))} +
+ )} + {applyState.error?.error_id && ( +

Error ID: {applyState.error.error_id}

+ )} +
+
+ )} +
+ ); +} + +// ─── DataPage ───────────────────────────────────────────────────────────────── diff --git a/client/components/data/ImportTransactionCsvSection.jsx b/client/components/data/ImportTransactionCsvSection.jsx new file mode 100644 index 0000000..9996ad0 --- /dev/null +++ b/client/components/data/ImportTransactionCsvSection.jsx @@ -0,0 +1,568 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { toast } from 'sonner'; +import { + Upload, Loader2, CheckCircle2, CheckCheck, AlertTriangle, Plus, FileText, +} from 'lucide-react'; +import { api } from '@/api'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { SectionCard } from './dataShared'; + +const CSV_MAPPING_FIELDS = [ + 'posted_date', + 'amount', + 'debit_amount', + 'credit_amount', + 'description', + 'payee', + 'memo', + 'category', + 'account', + 'transaction_id', + 'transaction_type', + 'currency', + 'transacted_at', +]; + +function compactMapping(mapping) { + return Object.fromEntries( + Object.entries(mapping || {}).filter(([, value]) => value), + ); +} + +function canCommitCsvMapping(mapping) { + return !!mapping?.posted_date && !!(mapping.amount || mapping.debit_amount || mapping.credit_amount); +} + +const CSV_IMPORT_STEPS = ['Upload', 'Preview', 'Map', 'Commit', 'Results']; + +function csvImportStepIndex(preview, mapping, commitState) { + if (commitState.status === 'done') return 4; + if (commitState.status === 'loading') return 3; + if (preview.status === 'ready') return canCommitCsvMapping(mapping) ? 3 : 2; + if (preview.status === 'loading' || preview.status === 'error') return 1; + return 0; +} + +function CsvImportStepper({ activeIndex }) { + return ( +
+ {CSV_IMPORT_STEPS.map((step, index) => { + const complete = index < activeIndex; + const active = index === activeIndex; + return ( +
+ + {complete ? : index + 1} + + {step} +
+ ); + })} +
+ ); +} + +function csvFieldRequirement(field, mapping) { + if (field === 'posted_date') return 'Required'; + if (['amount', 'debit_amount', 'credit_amount'].includes(field)) { + return canCommitCsvMapping({ ...mapping, posted_date: mapping?.posted_date || '__date__' }) + ? 'Amount source' + : 'One required'; + } + return 'Optional'; +} + +function csvFieldSamples(preview, header) { + if (!header) return []; + const values = []; + for (const row of preview?.sampleRows || []) { + const value = String(row?.[header] || '').trim(); + if (value && !values.includes(value)) values.push(value); + if (values.length >= 3) break; + } + return values; +} + +function CsvMappingRow({ field, label, preview, mapping, onChange, disabled = false }) { + const headers = preview?.headers || []; + const suggested = preview?.suggestedMapping?.[field] || ''; + const current = mapping[field] || ''; + const used = new Set(Object.entries(mapping) + .filter(([key, value]) => key !== field && value) + .map(([, value]) => value)); + const requirement = csvFieldRequirement(field, mapping); + const missingRequired = (requirement === 'Required' || requirement === 'One required') && !current; + const samples = csvFieldSamples(preview, current); + const suggestedAvailable = suggested && suggested !== current && !used.has(suggested); + + return ( +
+
+
+

{label}

+ + {requirement} + +
+

{field}

+
+ +
+ +
+ {current && current === suggested && ( + Suggested match + )} + {suggestedAvailable && !disabled && ( + + )} + {missingRequired && ( + Needs a column + )} +
+
+ +
+ {samples.length > 0 ? ( +
+ {samples.map(value => ( + + {value} + + ))} +
+ ) : ( +

+ {current ? 'No sample values' : 'Map a column to preview values'} +

+ )} +
+
+ ); +} + +function CsvMappingReview({ preview, fields, mapping, onMappingChange, onUseSuggested, onClearMapping, disabled = false }) { + const mappingFields = CSV_MAPPING_FIELDS.filter(field => fields[field]); + const mappedCount = mappingFields.filter(field => mapping[field]).length; + const hasSuggestedMapping = Object.values(preview?.suggestedMapping || {}).some(Boolean); + const missingRequired = [ + !mapping.posted_date ? 'Posted date' : null, + !(mapping.amount || mapping.debit_amount || mapping.credit_amount) ? 'Amount' : null, + ].filter(Boolean); + + return ( +
+
+
+

Column mapping

+

+ {mappedCount} of {mappingFields.length} fields mapped + {missingRequired.length > 0 && ` · missing ${missingRequired.join(' and ')}`} +

+
+
+ + +
+
+ +
+ Field + CSV Column + Sample Values +
+ +
+ {mappingFields.map(field => ( + + ))} +
+
+ ); +} + +function CsvSampleTable({ preview }) { + const headers = preview?.headers || []; + const sampleRows = preview?.sampleRows || []; + const visibleHeaders = headers.slice(0, 8); + const hiddenCount = Math.max(0, headers.length - visibleHeaders.length); + + if (sampleRows.length === 0) { + return

No sample rows found.

; + } + + return ( +
+ + + + {visibleHeaders.map(header => ( + + ))} + {hiddenCount > 0 && ( + + )} + + + + {sampleRows.map((row, index) => ( + + {visibleHeaders.map(header => ( + + ))} + {hiddenCount > 0 && ( + + )} + + ))} + +
{header}+{hiddenCount}
+ {row[header] || '—'} + more columns
+
+ ); +} + +function formatCsvRowDetail(detail) { + if (!detail) return ''; + const field = detail.field ? `${detail.field}: ` : ''; + return `${field}${detail.message || detail.value || JSON.stringify(detail)}`; +} + +export default function ImportTransactionCsvSection({ onHistoryRefresh }) { + const fileRef = useRef(null); + const [file, setFile] = useState(null); + const [preview, setPreview] = useState({ status: 'idle', data: null, error: null }); + const [mapping, setMapping] = useState({}); + const [commitState, setCommitState] = useState({ status: 'idle', result: null, error: null }); + + const reset = () => { + setFile(null); + setPreview({ status: 'idle', data: null, error: null }); + setMapping({}); + setCommitState({ status: 'idle', result: null, error: null }); + if (fileRef.current) fileRef.current.value = ''; + }; + + const handleMappingChange = (field, header) => { + if (commitState.status === 'done') return; + setMapping(prev => { + const next = { ...prev }; + if (header) next[field] = header; + else delete next[field]; + return next; + }); + setCommitState({ status: 'idle', result: null, error: null }); + }; + + const handlePreview = async () => { + if (!file) { + toast.error('Choose a CSV file first.'); + return; + } + setPreview({ status: 'loading', data: null, error: null }); + setMapping({}); + setCommitState({ status: 'idle', result: null, error: null }); + try { + const data = await api.previewCsvTransactionImport(file); + setPreview({ status: 'ready', data, error: null }); + setMapping(compactMapping(data.suggestedMapping || {})); + toast.success('CSV preview ready.'); + } catch (err) { + const errorState = importErrorState(err, 'CSV preview failed.'); + setPreview({ status: 'error', data: null, error: errorState }); + toast.error(errorState.message || 'CSV preview failed.'); + } + }; + + const handleCommit = async () => { + if (!preview.data?.import_session_id || !canCommitCsvMapping(mapping)) return; + setCommitState({ status: 'loading', result: null, error: null }); + try { + const result = await api.commitCsvTransactionImport({ + import_session_id: preview.data.import_session_id, + mapping: compactMapping(mapping), + }); + setCommitState({ status: 'done', result, error: null }); + toast.success(`CSV imported — ${result.imported} imported, ${result.skipped} skipped.`); + onHistoryRefresh?.(); + } catch (err) { + const errorState = importErrorState(err, 'CSV import failed.'); + setCommitState({ status: 'error', result: null, error: errorState }); + toast.error(errorState.message || 'CSV import failed.'); + } + }; + + const applySuggestedMapping = () => { + if (commitState.status === 'done') return; + setMapping(compactMapping(preview.data?.suggestedMapping || {})); + setCommitState({ status: 'idle', result: null, error: null }); + }; + + const clearMapping = () => { + if (commitState.status === 'done') return; + setMapping({}); + setCommitState({ status: 'idle', result: null, error: null }); + }; + + const fields = preview.data?.fields || {}; + const canCommit = preview.status === 'ready' + && preview.data?.import_session_id + && canCommitCsvMapping(mapping) + && commitState.status !== 'loading' + && commitState.status !== 'done'; + const activeStep = csvImportStepIndex(preview, mapping, commitState); + const failedRows = (commitState.result?.details || []).filter(d => d.result === 'failed'); + const skippedRows = (commitState.result?.details || []).filter(d => d.result === 'skipped_duplicate'); + + return ( + +
+
+
+ +
+

Import transaction rows from CSV.

+

+ This importer creates shared transaction records only. It does not match transactions to bills yet. +

+
+
+
+ + + +
+
+ +
+ + +
+
+
+ + {preview.status === 'error' && ( +
+ + {preview.error?.message || 'CSV preview failed.'} + {preview.error?.details?.length > 0 && ( +
    + {preview.error.details.map((d, i) => ( +
  • {d.message || JSON.stringify(d)}
  • + ))} +
+ )} +
+ )} + + {preview.status === 'ready' && preview.data && ( +
+
+
+
+

CSV Preview

+

{file?.name || 'Transaction CSV'}

+
+
+ + + +
+
+ + {preview.data.errors?.length > 0 && ( +
+

Review mapping

+
    + {preview.data.errors.map((issue, i) => ( +
  • + + {issue.message || JSON.stringify(issue)} +
  • + ))} +
+
+ )} + + +
+ + + +
+
+

+ {canCommitCsvMapping(mapping) ? 'Ready to commit' : 'Mapping incomplete'} +

+

+ Duplicates are skipped using a CSV transaction ID when available, otherwise a stable row hash. +

+
+ {commitState.status === 'done' ? ( + + ) : ( + + )} +
+
+ )} + + {commitState.status === 'done' && commitState.result && ( +
+
+ +

CSV transaction import complete

+
+
+ + + +
+ {skippedRows.length > 0 && ( +
+

Skipped duplicates ({skippedRows.length})

+
    + {skippedRows.map(row => ( +
  • + Row {row.row}: {row.provider_transaction_id} +
  • + ))} +
+
+ )} + {failedRows.length > 0 && ( +
+

Failed rows ({failedRows.length})

+
    + {failedRows.map((row, index) => ( +
  • +

    Row {row.row}: {row.message}

    + {row.details?.length > 0 && ( +
      + {row.details.map((detail, detailIndex) => ( +
    • {formatCsvRowDetail(detail)}
    • + ))} +
    + )} +
  • + ))} +
+
+ )} +
+ )} + + {commitState.status === 'error' && ( +
+ + {commitState.error?.message || 'CSV import failed.'} + {commitState.error?.details?.length > 0 && ( +
    + {commitState.error.details.map((d, i) => ( +
  • {d.message || JSON.stringify(d)}
  • + ))} +
+ )} +
+ )} +
+
+ ); +} + +// ─── Section 3: Import My Data Export ──────────────────────────────────────── diff --git a/client/components/data/SeedDemoDataSection.jsx b/client/components/data/SeedDemoDataSection.jsx new file mode 100644 index 0000000..5de52de --- /dev/null +++ b/client/components/data/SeedDemoDataSection.jsx @@ -0,0 +1,120 @@ +import React, { useState, useEffect } from 'react'; +import { toast } from 'sonner'; +import { Loader2 } from 'lucide-react'; +import { api } from '@/api'; +import { Button } from '@/components/ui/button'; +import { + AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, + AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog'; +import { SectionCard } from './dataShared'; + +export default function SeedDemoDataSection({ onSeeded }) { + const [loading, setLoading] = useState(false); + const [seeded, setSeeded] = useState(false); + const [counts, setCounts] = useState({ bills: 0, categories: 0 }); + const [clearing, setClearing] = useState(false); + const [showClearConfirm, setShowClearConfirm] = useState(false); + const [statusLoading, setStatusLoading] = useState(true); + + useEffect(() => { + api.seededStatus() + .then(data => { + setSeeded(data.seeded); + if (data.seeded) setCounts({ bills: data.seededBills || 0, categories: data.seededCategories || 0 }); + }) + .catch(err => console.error('Failed to check seeded status:', err)) + .finally(() => setStatusLoading(false)); + }, []); + + const handleSeed = async () => { + setLoading(true); + try { + const data = await api.seedDemoData(); + if (!data || typeof data !== 'object') throw new Error('Invalid response from server'); + setCounts({ bills: data.billsCreated || 0, categories: data.categoriesCreated || 0 }); + setSeeded(true); + toast.success(`Created ${data.billsCreated || 0} demo bills successfully.`); + setTimeout(() => onSeeded?.(), 100); + } catch (err) { + console.error('Seed error:', err); + toast.error(err?.message || err?.error || 'Failed to seed demo data.'); + } finally { + setLoading(false); + } + }; + + const handleClearDemoData = async () => { + setClearing(true); + try { + const data = await api.clearDemoData(); + setSeeded(false); + setCounts({ bills: 0, categories: 0 }); + setShowClearConfirm(false); + toast.success(`Removed ${data.billsDeleted || 0} demo bills and ${data.categoriesDeleted || 0} demo categories.`); + onSeeded?.(); + } catch (err) { + toast.error(err.message || 'Failed to clear demo data.'); + } finally { + setClearing(false); + } + }; + + return ( + +
+ {statusLoading ? ( +

Loading…

+ ) : seeded ? ( + <> +

Demo data seeded

+
+
+

Bills

+

{counts.bills}

+
+
+

Categories

+

{counts.categories}

+
+
+ + ) : ( +

+ Create 20 realistic demo bills and 8 demo categories for testing purposes. + The data will be associated with your account. +

+ )} + +
+ + + + + + + + + Clear Demo Data + + This will remove {counts.bills} demo bills and {counts.categories} demo categories from your account. This cannot be undone. + + + + Cancel + + {clearing ? <>Clearing… : 'Clear Data'} + + + + +
+
+
+ ); +} diff --git a/client/components/data/TransactionMatchingSection.jsx b/client/components/data/TransactionMatchingSection.jsx new file mode 100644 index 0000000..4098211 --- /dev/null +++ b/client/components/data/TransactionMatchingSection.jsx @@ -0,0 +1,562 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { toast } from 'sonner'; +import { + Sparkles, CheckCircle2, Loader2, RefreshCw, Link2, Link2Off, + XCircle, Eye, EyeOff, Search, +} from 'lucide-react'; +import { api } from '@/api'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { + Dialog, DialogContent, DialogDescription, DialogFooter, + DialogHeader, DialogTitle, +} from '@/components/ui/dialog'; +import { SectionCard } from './dataShared'; + +const TRANSACTION_FILTERS = [ + { id: 'open', label: 'Open', params: { ignored: 'false' } }, + { id: 'unmatched', label: 'Unmatched', params: { match_status: 'unmatched', ignored: 'false' } }, + { id: 'matched', label: 'Matched', params: { match_status: 'matched', ignored: 'false' } }, + { id: 'ignored', label: 'Ignored', params: { match_status: 'ignored', ignored: 'true' } }, + { id: 'all', label: 'All', params: { ignored: 'all' } }, +]; + +function transactionStatus(tx) { + if (tx?.ignored) return 'ignored'; + return tx?.match_status || 'unmatched'; +} + +function TransactionStatusBadge({ tx }) { + const status = transactionStatus(tx); + const styles = { + matched: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600', + ignored: 'border-muted-foreground/30 bg-muted/40 text-muted-foreground', + unmatched: 'border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400', + }; + + return ( + + {status} + + ); +} + +function formatTransactionAmount(amount, currency = 'USD') { + const value = Math.abs(Number(amount || 0)) / 100; + const sign = Number(amount || 0) < 0 ? '-' : '+'; + return `${sign}${new Intl.NumberFormat(undefined, { + style: 'currency', + currency: currency || 'USD', + }).format(value)}`; +} + +function transactionDate(tx) { + return tx?.posted_date || String(tx?.transacted_at || '').slice(0, 10) || '—'; +} + +function transactionTitle(tx) { + return tx?.payee || tx?.description || tx?.memo || 'Transaction'; +} + +function matchScoreTone(score) { + const value = Number(score) || 0; + if (value >= 80) return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'; + if (value >= 55) return 'border-sky-500/30 bg-sky-500/10 text-sky-600 dark:text-sky-400'; + return 'border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400'; +} + +function SuggestedMatchesPanel({ suggestions, loading, actionId, onAccept, onReject }) { + return ( +
+
+
+ + + +
+

Suggested matches

+

{loading ? 'Checking transactions' : `${suggestions.length} ready for review`}

+
+
+
+ + {loading ? ( +
+ + Finding likely bill matches... +
+ ) : suggestions.length === 0 ? ( +
+ No suggested matches right now. +
+ ) : ( +
+ {suggestions.map(suggestion => { + const tx = suggestion.transaction || {}; + const bill = suggestion.bill || {}; + const acceptBusy = actionId === `suggestion-match:${suggestion.id}`; + const rejectBusy = actionId === `suggestion-reject:${suggestion.id}`; + const busy = acceptBusy || rejectBusy; + + return ( +
+
+
+
+ + {suggestion.score} + +

{transactionTitle(tx)}

+
+

+ {transactionDate(tx)} · {tx.source_label || tx.source_type_label || 'Transaction'} +

+
+

+ {formatTransactionAmount(tx.amount, tx.currency)} +

+
+ +
+ +
+

{bill.name || `Bill ${suggestion.billId}`}

+

+ Expected ${Number(bill.expected_amount || 0).toFixed(2)} +

+
+
+ + {suggestion.reasons?.length > 0 && ( +
+ {suggestion.reasons.slice(0, 4).map(reason => ( + + {reason} + + ))} +
+ )} + +
+ + +
+
+ ); + })} +
+ )} +
+ ); +} + +function MatchBillDialog({ open, onOpenChange, transaction, bills, onConfirm, loading }) { + const [query, setQuery] = useState(''); + const [selectedBillId, setSelectedBillId] = useState(''); + + useEffect(() => { + if (open) { + setQuery(''); + setSelectedBillId(transaction?.matched_bill_id ? String(transaction.matched_bill_id) : ''); + } + }, [open, transaction?.id, transaction?.matched_bill_id]); + + const filteredBills = useMemo(() => { + const q = query.trim().toLowerCase(); + if (!q) return bills.slice(0, 40); + return bills + .filter(bill => String(bill.name || '').toLowerCase().includes(q)) + .slice(0, 40); + }, [bills, query]); + + const selectedBill = bills.find(bill => String(bill.id) === String(selectedBillId)); + + return ( + + + + Match Transaction + + Choose the bill this transaction paid. Nothing changes until you confirm. + + + + {transaction && ( +
+
+
+

{transactionTitle(transaction)}

+

+ {transactionDate(transaction)} · {transaction.source_label || transaction.source_type_label || 'Transaction'} +

+
+

+ {formatTransactionAmount(transaction.amount, transaction.currency)} +

+
+ {transaction.description && transaction.description !== transactionTitle(transaction) && ( +

{transaction.description}

+ )} +
+ )} + +
+ + +
+ {filteredBills.length === 0 ? ( +

No bills found.

+ ) : ( +
+ {filteredBills.map(bill => ( + + ))} +
+ )} +
+
+ + + + + +
+
+ ); +} + +export default function TransactionMatchingSection({ refreshKey }) { + const [transactions, setTransactions] = useState([]); + const [suggestions, setSuggestions] = useState([]); + const [bills, setBills] = useState([]); + const [filter, setFilter] = useState('open'); + const [loading, setLoading] = useState(true); + const [suggestionsLoading, setSuggestionsLoading] = useState(true); + const [billsLoading, setBillsLoading] = useState(true); + const [actionId, setActionId] = useState(null); + const [matchOpen, setMatchOpen] = useState(false); + const [matchTransaction, setMatchTransaction] = useState(null); + + const currentFilter = TRANSACTION_FILTERS.find(item => item.id === filter) || TRANSACTION_FILTERS[0]; + + const loadTransactions = async () => { + setLoading(true); + try { + const data = await api.transactions({ limit: 100, ...currentFilter.params }); + setTransactions(data || []); + } catch (err) { + toast.error(err.message || 'Failed to load transactions.'); + setTransactions([]); + } finally { + setLoading(false); + } + }; + + const loadSuggestions = async () => { + setSuggestionsLoading(true); + try { + const data = await api.matchSuggestions({ limit: 8 }); + setSuggestions(data || []); + } catch (err) { + toast.error(err.message || 'Failed to load match suggestions.'); + setSuggestions([]); + } finally { + setSuggestionsLoading(false); + } + }; + + const refreshTransactionWorkbench = async () => { + await Promise.all([loadTransactions(), loadSuggestions()]); + }; + + const loadBills = async () => { + setBillsLoading(true); + try { + const data = await api.bills(); + setBills(data || []); + } catch { + setBills([]); + } finally { + setBillsLoading(false); + } + }; + + useEffect(() => { loadBills(); }, []); + useEffect(() => { loadTransactions(); }, [filter, refreshKey]); + useEffect(() => { loadSuggestions(); }, [refreshKey]); + + const openMatchDialog = (tx) => { + setMatchTransaction(tx); + setMatchOpen(true); + if (!bills.length && !billsLoading) loadBills(); + }; + + const runTransactionAction = async (tx, action) => { + setActionId(`${action}:${tx.id}`); + try { + if (action === 'unmatch') { + await api.unmatchTransaction(tx.id); + toast.success('Transaction unmatched.'); + } else if (action === 'ignore') { + await api.ignoreTransaction(tx.id); + toast.success('Transaction ignored.'); + } else if (action === 'unignore') { + await api.unignoreTransaction(tx.id); + toast.success('Transaction restored.'); + } + await refreshTransactionWorkbench(); + } catch (err) { + toast.error(err.message || 'Transaction action failed.'); + } finally { + setActionId(null); + } + }; + + const confirmMatch = async (billId) => { + if (!matchTransaction) return; + setActionId(`match:${matchTransaction.id}`); + try { + await api.matchTransaction(matchTransaction.id, billId); + toast.success('Transaction matched to bill.'); + setMatchOpen(false); + setMatchTransaction(null); + await refreshTransactionWorkbench(); + } catch (err) { + toast.error(err.message || 'Transaction match failed.'); + } finally { + setActionId(null); + } + }; + + const acceptSuggestion = async (suggestion) => { + setActionId(`suggestion-match:${suggestion.id}`); + try { + await api.matchTransaction(suggestion.transactionId, suggestion.billId); + toast.success('Suggested match confirmed.'); + await refreshTransactionWorkbench(); + } catch (err) { + toast.error(err.message || 'Suggested match failed.'); + } finally { + setActionId(null); + } + }; + + const rejectSuggestion = async (suggestion) => { + setActionId(`suggestion-reject:${suggestion.id}`); + try { + await api.rejectMatchSuggestion(suggestion.id); + toast.success('Suggestion rejected.'); + await loadSuggestions(); + } catch (err) { + toast.error(err.message || 'Suggestion could not be rejected.'); + } finally { + setActionId(null); + } + }; + + return ( + +
+
+
+ {TRANSACTION_FILTERS.map(item => ( + + ))} +
+ +
+ + + +
+ {loading ? ( +
Loading transactions…
+ ) : transactions.length === 0 ? ( +
+ No transactions found for this filter. +
+ ) : ( + + + + + + + + + + + + {transactions.map(tx => { + const status = transactionStatus(tx); + const busy = actionId?.endsWith(`:${tx.id}`); + return ( + + + + + + + + ); + })} + +
DateTransactionMatchAmountActions
+ {transactionDate(tx)} + +
+

{transactionTitle(tx)}

+

+ {[tx.description, tx.account_name, tx.source_label].filter(Boolean).join(' · ') || '—'} +

+
+
+
+ + {tx.matched_bill_name ? ( + {tx.matched_bill_name} + ) : ( + No bill linked + )} +
+
+ {formatTransactionAmount(tx.amount, tx.currency)} + +
+ {status === 'ignored' ? ( + + ) : ( + <> + {status === 'matched' ? ( + + ) : ( + + )} + + + )} +
+
+ )} +
+
+ + +
+ ); +} diff --git a/client/components/data/dataShared.jsx b/client/components/data/dataShared.jsx new file mode 100644 index 0000000..d5c7932 --- /dev/null +++ b/client/components/data/dataShared.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { RefreshCw } from 'lucide-react'; + +export function fmt(isoStr) { + if (!isoStr) return '—'; + const d = new Date(isoStr); + return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) + + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); +} + +export function importErrorState(err, fallback) { + const data = err?.data || {}; + return { + message: err?.message || data.message || data.error || fallback, + error: data.error || fallback, + code: data.code || err?.code || null, + details: Array.isArray(data.details) ? data.details : (Array.isArray(err?.details) ? err.details : []), + error_id: data.error_id || null, + }; +} + +export function SectionCard({ title, subtitle, children, className }) { + return ( +
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+
{children}
+
+ ); +} + +export function CountPill({ label, value }) { + return ( +
+

{label}

+

{value ?? 0}

+
+ ); +} diff --git a/client/components/tracker/MonthlyStateDialog.jsx b/client/components/tracker/MonthlyStateDialog.jsx new file mode 100644 index 0000000..4d8d9c4 --- /dev/null +++ b/client/components/tracker/MonthlyStateDialog.jsx @@ -0,0 +1,136 @@ +import React, { useState, useEffect } from 'react'; +import { toast } from 'sonner'; +import { api } from '@/api.js'; +import { fmt } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, +} from '@/components/ui/dialog'; + +const MONTHS = [ + 'January','February','March','April','May','June', + 'July','August','September','October','November','December', +]; + +function MonthlyStateDialog({ row, year, month, open, onOpenChange, onSaved }) { + const [actualAmount, setActualAmount] = useState(''); + const [notes, setNotes] = useState(''); + const [isSkipped, setIsSkipped] = useState(false); + const [saving, setSaving] = useState(false); + + // Populate from current row state when dialog opens + useEffect(() => { + if (open) { + setActualAmount(row.actual_amount != null ? String(row.actual_amount) : ''); + setNotes(row.monthly_notes || ''); + setIsSkipped(!!row.is_skipped); + } + }, [open, row]); + + async function handleSave(e) { + e.preventDefault(); + const amt = actualAmount.trim() ? parseFloat(actualAmount) : null; + if (amt !== null && (isNaN(amt) || amt < 0)) { + toast.error('Amount must be a positive number or empty'); + return; + } + setSaving(true); + try { + await api.saveBillMonthlyState(row.id, { + year, + month, + actual_amount: amt, + notes: notes.trim() || null, + is_skipped: isSkipped, + }); + toast.success(`${MONTHS[month - 1]} state saved`); + onSaved(); + onOpenChange(false); + } catch (err) { + toast.error(err.message); + } finally { + setSaving(false); + } + } + + return ( + + + + + {row.name} + + {MONTHS[month - 1]} {year} + + +

+ Monthly overrides — changes only affect {MONTHS[month - 1]} +

+
+ +
+ {/* Actual amount this month */} +
+ + setActualAmount(e.target.value)} + className="font-mono bg-background/50 border-border/60" + /> +

+ Leave blank to use the template default ({fmt(row.expected_amount)}). +

+
+ + {/* Monthly notes */} +
+ + setNotes(e.target.value)} + placeholder="e.g. higher than usual, double-billed…" + className="bg-background/50 border-border/60" + /> +
+ + {/* Skip this month */} + +
+ + + + + +
+
+ ); +} + +export default MonthlyStateDialog; diff --git a/client/components/tracker/PaymentModal.jsx b/client/components/tracker/PaymentModal.jsx new file mode 100644 index 0000000..7dc2619 --- /dev/null +++ b/client/components/tracker/PaymentModal.jsx @@ -0,0 +1,154 @@ +import React, { useState } from 'react'; +import { toast } from 'sonner'; +import { api } from '@/api.js'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, +} from '@/components/ui/dialog'; +import { + AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, + AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, +} from '@/components/ui/alert-dialog'; +import { + Select, SelectTrigger, SelectValue, SelectContent, SelectItem, +} from '@/components/ui/select'; + +const METHOD_NONE = 'none'; + +function PaymentModal({ payment, onClose, onSave }) { + const [amount, setAmount] = useState(String(payment.amount)); + const [date, setDate] = useState(payment.paid_date); + // Use METHOD_NONE sentinel — empty string value crashes Radix Select + const [method, setMethod] = useState(payment.method || METHOD_NONE); + const [notes, setNotes] = useState(payment.notes || ''); + const [busy, setBusy] = useState(false); + const [confirmDelete, setConfirmDelete] = useState(false); + + async function handleSave(e) { + e.preventDefault(); + setBusy(true); + try { + await api.updatePayment(payment.id, { + amount: parseFloat(amount), + paid_date: date, + method: method === METHOD_NONE ? null : method, + notes: notes || null, + }); + toast.success('Payment saved'); + onSave(); onClose(); + } catch (err) { + toast.error(err.message); + } finally { setBusy(false); } + } + + async function handleDelete() { + setBusy(true); + try { + await api.deletePayment(payment.id); + toast.success('Payment moved to recovery. Bill is now marked as unpaid.', { + action: { + label: 'Undo', + onClick: async () => { + try { + await api.restorePayment(payment.id); + toast.success('Payment restored'); + onSave(); + } catch (err) { + toast.error(err.message || 'Failed to restore payment'); + } + }, + }, + }); + onSave(); onClose(); + } catch (err) { + toast.error(err.message); + } finally { setBusy(false); } + } + + return ( + <> + { if (!v) onClose(); }}> + + + Edit Payment + + +
+
+ + setAmount(e.target.value)} + className="font-mono bg-background/50 border-border/60" /> +
+
+ + setDate(e.target.value)} + className="font-mono bg-background/50 border-border/60" /> +
+
+ + +
+
+ + setNotes(e.target.value)} + className="bg-background/50 border-border/60" /> +
+
+ + + +
+ + +
+
+
+
+ + + + + Remove this payment? + + This marks the payment as removed and reverses any debt balance update. You can undo it from the toast. + + + + Cancel + + {busy ? 'Removing...' : 'Remove Payment'} + + + + + + ); +} + +export default PaymentModal; diff --git a/client/components/tracker/StartingAmountsEditDialog.jsx b/client/components/tracker/StartingAmountsEditDialog.jsx new file mode 100644 index 0000000..e56c352 --- /dev/null +++ b/client/components/tracker/StartingAmountsEditDialog.jsx @@ -0,0 +1,197 @@ +import React, { useState, useEffect } from 'react'; +import { toast } from 'sonner'; +import { api } from '@/api.js'; +import { fmt } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, +} from '@/components/ui/dialog'; + +const MONTHS = [ + 'January','February','March','April','May','June', + 'July','August','September','October','November','December', +]; + +function StartingAmountsEditDialog({ open, onClose, year, month, onSave }) { + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(''); + const [firstAmount, setFirstAmount] = useState('0'); + const [fifteenthAmount, setFifteenthAmount] = useState('0'); + const [otherAmount, setOtherAmount] = useState('0'); + const [preview, setPreview] = useState(null); + + const monthName = `${MONTHS[month - 1]} ${year}`; + const localFirst = Number(firstAmount) || 0; + const localFifteenth = Number(fifteenthAmount) || 0; + const localOther = Number(otherAmount) || 0; + const totalStarting = localFirst + localFifteenth + localOther; + const paidSoFar = Number(preview?.paid_total || 0); + const firstRemaining = localFirst - Number(preview?.paid_from_first || 0); + const fifteenthRemaining = localFifteenth - Number(preview?.paid_from_fifteenth || 0); + const totalRemaining = totalStarting - paidSoFar; + + useEffect(() => { + let alive = true; + async function loadStartingAmounts() { + if (!open) return; + setLoading(true); + setError(''); + try { + const result = await api.getMonthlyStartingAmounts(year, month); + if (!alive) return; + setPreview(result); + setFirstAmount(String(result.first_amount ?? 0)); + setFifteenthAmount(String(result.fifteenth_amount ?? 0)); + setOtherAmount(String(result.other_amount ?? 0)); + } catch (err) { + if (!alive) return; + setError(err.message || 'Monthly starting amounts could not be loaded.'); + } finally { + if (alive) setLoading(false); + } + } + loadStartingAmounts(); + return () => { alive = false; }; + }, [open, year, month]); + + async function handleSave(e) { + e.preventDefault(); + const first = Number(firstAmount); + const fifteenth = Number(fifteenthAmount); + const other = Number(otherAmount); + if (![first, fifteenth, other].every(value => Number.isFinite(value) && value >= 0)) { + setError('Starting amounts must be non-negative numbers.'); + return; + } + + setSaving(true); + setError(''); + try { + await api.updateMonthlyStartingAmounts({ + year, + month, + first_amount: first, + fifteenth_amount: fifteenth, + other_amount: other, + }); + toast.success('Monthly starting amounts saved.'); + onSave(); + } catch (err) { + setError(err.message || 'Monthly starting amounts could not be saved.'); + } finally { + setSaving(false); + } + } + + return ( + { if (!value) onClose(); }}> + + + Monthly Starting Amounts +

{monthName}

+
+ +
+ {error && ( +
+ {error} +
+ )} + +
+ + + +
+ +
+
+
+

Total starting

+

{fmt(totalStarting)}

+
+
+

Paid so far

+

{fmt(paidSoFar)}

+
+
+

Total remaining

+

{fmt(totalRemaining)}

+
+
+
+
+ 1st remaining + {fmt(firstRemaining)} +
+
+ 15th remaining + {fmt(fifteenthRemaining)} +
+
+ Other + {fmt(localOther)} +
+
+
+
+ + + + + +
+
+ ); +} + +export default StartingAmountsEditDialog; diff --git a/client/pages/AdminPage.jsx b/client/pages/AdminPage.jsx index 42e0536..74ec73e 100644 --- a/client/pages/AdminPage.jsx +++ b/client/pages/AdminPage.jsx @@ -1,1843 +1,21 @@ import React, { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; -import { toast } from 'sonner'; -import { - ChevronLeft, ChevronRight, Database, Download, Eye, EyeOff, - Play, RefreshCw, RotateCcw, Trash2, Upload, Wrench, -} from 'lucide-react'; import { api } from '@/api'; -import { cn, fmtBytes } from '@/lib/utils'; -import { Button, buttonVariants } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Badge } from '@/components/ui/badge'; -import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; -import { - Select, SelectTrigger, SelectValue, SelectContent, SelectItem, -} from '@/components/ui/select'; -import { - AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, - AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, -} from '@/components/ui/alert-dialog'; import AppNavigation from '@/components/layout/Sidebar'; - -// ─── Helpers ────────────────────────────────────────────────────────────────── - -const AUTHENTIK_ICON_URL = '/img/auth.png'; - -function SectionHeading({ children }) { - return

{children}

; -} - -function FieldRow({ label, children }) { - return ( -
- - {children} -
- ); -} - -function Toggle({ checked, onChange, label, disabled = false }) { - return ( - - ); -} - -function defaultOidcRedirectUri() { - if (typeof window === 'undefined') return ''; - return `${window.location.origin}/api/auth/oidc/callback`; -} - -function looksLikeOidcEndpoint(url) { - const value = String(url || '').toLowerCase(); - return /\/(?:authorize|token|userinfo|jwks|certs)\/?$/.test(value); -} - -function formatDateTime(value) { - if (!value) return '—'; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return value; - return date.toLocaleString(); -} - -function BackupTypeBadge({ type }) { - const cls = { - manual: 'bg-blue-500/15 text-blue-400 border-blue-500/20', - scheduled: 'bg-emerald-500/15 text-emerald-400 border-emerald-500/20', - imported: 'bg-violet-500/15 text-violet-400 border-violet-500/20', - 'pre-restore': 'bg-amber-500/15 text-amber-400 border-amber-500/20', - }[type] || 'bg-muted text-muted-foreground border-border'; - - return {type || 'backup'}; -} - -// ─── Onboarding Wizard ──────────────────────────────────────────────────────── - -function OnboardingWizard({ onComplete }) { - const [step, setStep] = useState(0); - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [confirm, setConfirm] = useState(''); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - - const handleCreate = async (e) => { - e.preventDefault(); - setError(''); - - let validationError = ''; - if (password !== confirm) { - validationError = 'Passwords do not match.'; - } else if (password.length < 8) { - validationError = 'Password must be at least 8 characters.'; - } - - if (validationError) { - setError(validationError); - toast.error(validationError); - return; - } - - setLoading(true); - try { - await api.createUser({ username, password }); - toast.success('User created successfully.'); - onComplete(); - } catch (err) { - const errorMessage = err.message || 'Failed to create user.'; - setError(errorMessage); - toast.error(errorMessage); - } finally { - setLoading(false); - } - }; - - return ( -
-
- {/* Step dots */} -
- {[0, 1].map(i => ( - - ))} -
- - {step === 0 && ( - - - Welcome, Administrator -

- Before creating your first user, please understand what your admin account can and cannot do. -

-
- -
- {[ - { can: true, text: 'Create and manage user accounts' }, - { can: true, text: 'Reset user passwords' }, - { can: true, text: 'Configure email notifications' }, - { can: true, text: 'Toggle single-user / multi-user mode' }, - { can: false, text: 'Cannot view bills or financial data' }, - { can: false, text: 'Cannot access user settings or history' }, - ].map(({ can, text }) => ( -
- - {can ? '✓' : '✗'} - - {text} -
- ))} -
-
- -
-
-
- )} - - {step === 1 && ( - - - Create first user -

- This account will be used to access the bill tracker. -

-
- -
-
- - setUsername(e.target.value)} - required - /> -
-
- - setPassword(e.target.value)} - required - /> -
-
- - setConfirm(e.target.value)} - required - /> -
- {error && ( -
- {error} -
- )} -
- - -
-
-
-
- )} -
-
- ); -} - -// ─── Email Notifications Card ───────────────────────────────────────────────── - -function EmailNotifCard() { - const DEFAULTS = { - enabled: false, - sender_name: '', sender_address: '', - smtp_host: '', smtp_port: '587', smtp_encryption: 'starttls', - smtp_self_signed: false, - smtp_username: '', smtp_password: '', - allow_user_config: false, - global_recipient: '', - }; - - const [cfg, setCfg] = useState(DEFAULTS); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [showPw, setShowPw] = useState(false); - const [testEmail, setTestEmail] = useState(''); - const [testing, setTesting] = useState(false); - - useEffect(() => { - api.notifAdmin() - .then(d => setCfg({ ...DEFAULTS, ...d })) - .catch(() => {}) - .finally(() => setLoading(false)); - }, []); // eslint-disable-line react-hooks/exhaustive-deps - - const set = (k, v) => setCfg(p => ({ ...p, [k]: v })); - - const handleSave = async () => { - setSaving(true); - try { - await api.saveNotifAdmin(cfg); - toast.success('Email settings saved.'); - } catch (err) { - toast.error(err.message || 'Failed to save.'); - } finally { - setSaving(false); - } - }; - - const handleTest = async () => { - if (!testEmail) { toast.error('Enter a recipient email.'); return; } - setTesting(true); - try { - await api.testEmail({ to: testEmail }); - toast.success('Test email sent.'); - } catch (err) { - toast.error(err.message || 'Failed to send test email.'); - } finally { - setTesting(false); - } - }; - - if (loading) return Loading…; - - return ( - - - Email Notifications - - - {/* Enable toggle */} -
-
-

Enable email notifications

-

Configure SMTP to send bill reminders

-
- set('enabled', v)} label="Enable email notifications" /> -
- -
- -
- Sender - - set('sender_name', e.target.value)} placeholder="BillTracker" /> - - - set('sender_address', e.target.value)} placeholder="no-reply@example.com" type="email" /> - -
- -
- -
- SMTP Server - - set('smtp_host', e.target.value)} placeholder="smtp.example.com" /> - - - set('smtp_port', e.target.value)} placeholder="587" type="number" className="w-28" /> - - - - - -
- set('smtp_self_signed', e.target.checked)} - className="h-4 w-4 rounded border-input bg-input accent-primary" - /> - -
-
- - set('smtp_username', e.target.value)} placeholder="user@example.com" /> - - -
- set('smtp_password', e.target.value)} - placeholder="••••••••" - className="pr-9" - /> - -
-
-
- -
- -
- User Access - -
- set('allow_user_config', e.target.checked)} - className="h-4 w-4 rounded border-input bg-input accent-primary" - /> - -
-
- - set('global_recipient', e.target.value)} - placeholder="recipient@example.com" - type="email" - /> - -
- -
- -
- Test Email - -
- setTestEmail(e.target.value)} - placeholder="you@example.com" - type="email" - /> - -
-
-
- -
- -
- - - ); -} - -// ─── Login Mode Card ────────────────────────────────────────────────────────── - -function LoginModeCard({ users }) { - const [modeData, setModeData] = useState(null); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [selectedUser, setSelectedUser] = useState(''); - - // Single-user mode confirmation dialog - const [confirmSingle, setConfirmSingle] = useState(false); - const [pendingUserId, setPendingUserId] = useState(null); - - useEffect(() => { - api.authModeConfig() - .then(d => { setModeData(d); setSelectedUser(d.default_user_id?.toString() || ''); }) - .catch(() => {}) - .finally(() => setLoading(false)); - }, []); - - const doSetMode = async (mode, userId) => { - setSaving(true); - try { - await api.setAuthMode({ - auth_mode: mode, - default_user_id: mode === 'single' ? parseInt(userId, 10) : null, - }); - const d = await api.authModeConfig(); - setModeData(d); - toast.success(mode === 'single' ? 'Single-user mode enabled.' : 'Login requirement restored.'); - } catch (err) { - toast.error(err.message || 'Failed to update auth mode.'); - } finally { - setSaving(false); - } - }; - - const handleRequestSingle = () => { - if (!selectedUser) { toast.error('Select a user first.'); return; } - setPendingUserId(selectedUser); - setConfirmSingle(true); - }; - - const handleConfirmSingle = () => { - setConfirmSingle(false); - doSetMode('single', pendingUserId); - }; - - if (loading) return Loading…; - - const isMulti = !modeData || modeData.auth_mode === 'multi'; - const activeUser = users?.find(u => u.id === modeData?.default_user_id); - const selectedUsername = users?.find(u => u.id.toString() === selectedUser)?.username ?? selectedUser; - - return ( - <> - - -
- Login Mode - - {isMulti ? 'Multi-user' : 'Single-user'} - -
-
- - {isMulti ? ( - <> -

- Single-user mode bypasses the login screen and automatically signs in as the selected user. -

-
- - -
- - - ) : ( - <> -

- Currently auto-signing in as{' '} - {activeUser?.username ?? '—'}. - Restoring login requirement will require all users to sign in manually. -

- - - )} -
-
- - {/* Single-user mode confirmation */} - - - - Enable Single-User Mode? - - Anyone who opens the app will be automatically signed in as{' '} - {selectedUsername}. - The admin login still requires a password. - - - - Cancel - - Enable Single-User Mode - - - - - - ); -} - -// ─── AuthMethodsCard ───────────────────────────────────────────────────────── -// Controls login methods and DB-backed authentik/OIDC provider settings. -// Client secret is write-only; the API returns only a "set" marker. - -function AuthMethodsCard() { - const [data, setData] = useState(null); - const [form, setForm] = useState(null); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [testingOidc, setTestingOidc] = useState(false); - const [oidcTest, setOidcTest] = useState(null); - - const load = useCallback(async () => { - try { - const d = await api.authModeConfig(); - setData(d); - setForm({ - local_login_enabled: d.local_login_enabled !== false, - oidc_login_enabled: !!d.oidc_login_enabled, - oidc_provider_name: d.oidc_provider_name || 'authentik', - oidc_issuer_url: d.oidc_issuer_url || '', - oidc_client_id: d.oidc_client_id || '', - oidc_client_secret: '', - oidc_client_secret_clear: false, - oidc_token_auth_method: d.oidc_token_auth_method || 'client_secret_basic', - oidc_redirect_uri: d.oidc_redirect_uri || defaultOidcRedirectUri(), - oidc_scopes: d.oidc_scopes || 'openid email profile groups', - oidc_auto_provision: d.oidc_auto_provision !== false, - oidc_admin_group: d.oidc_admin_group || '', - oidc_default_role: d.oidc_default_role || 'user', - }); - } catch (err) { - toast.error(err.message || 'Failed to load auth settings.'); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { load(); }, [load]); - - const set = (k, v) => setForm(prev => ({ ...prev, [k]: v })); - - async function handleSave() { - setSaving(true); - try { - const d = await api.setAuthMode(form); - setData(d); - setForm({ - local_login_enabled: d.local_login_enabled !== false, - oidc_login_enabled: !!d.oidc_login_enabled, - oidc_provider_name: d.oidc_provider_name || 'authentik', - oidc_issuer_url: d.oidc_issuer_url || '', - oidc_client_id: d.oidc_client_id || '', - oidc_client_secret: '', - oidc_client_secret_clear: false, - oidc_token_auth_method: d.oidc_token_auth_method || 'client_secret_basic', - oidc_redirect_uri: d.oidc_redirect_uri || defaultOidcRedirectUri(), - oidc_scopes: d.oidc_scopes || 'openid email profile groups', - oidc_auto_provision: d.oidc_auto_provision !== false, - oidc_admin_group: d.oidc_admin_group || '', - oidc_default_role: d.oidc_default_role || 'user', - }); - toast.success('Auth method settings saved.'); - } catch (err) { - toast.error(err.message || 'Failed to save auth method settings.'); - } finally { - setSaving(false); - } - } - - async function handleTestOidc() { - setTestingOidc(true); - setOidcTest(null); - try { - const result = await api.testOidcConfig(form); - setOidcTest(result); - toast.success('authentik configuration test passed.'); - } catch (err) { - const result = err.data || { ok: false, error: err.message || 'OIDC configuration test failed.' }; - setOidcTest(result); - toast.error(result.error || 'OIDC configuration test failed.'); - } finally { - setTestingOidc(false); - } - } - - if (loading || !form) { - return ( - - - Loading auth settings… - - - ); - } - - const secretAvailable = form.oidc_client_secret.trim() - ? true - : form.oidc_client_secret_clear - ? false - : !!data?.oidc_client_secret_set; - const oidcConfigured = !!( - form.oidc_issuer_url.trim() && - form.oidc_client_id.trim() && - secretAvailable && - form.oidc_redirect_uri.trim() - ); - const adminGroupConfigured = !!form.oidc_admin_group.trim(); - const wouldLockOut = !form.local_login_enabled && !form.oidc_login_enabled; - const cantDisableLocal = !form.local_login_enabled && (!oidcConfigured || !form.oidc_login_enabled || !adminGroupConfigured); - const oidcEnabledButIncomplete = form.oidc_login_enabled && !oidcConfigured; - const canSave = !wouldLockOut && !cantDisableLocal && !oidcEnabledButIncomplete && !saving; - const canTestOidc = oidcConfigured && !testingOidc; - const missingFields = [ - !form.oidc_issuer_url.trim() && 'Issuer URL', - !form.oidc_client_id.trim() && 'Client ID', - !secretAvailable && 'Client Secret', - !form.oidc_redirect_uri.trim() && 'Redirect URI', - ].filter(Boolean); - const issuerEndpointWarning = looksLikeOidcEndpoint(form.oidc_issuer_url); - - return ( - - -
-
- Authentication Methods -

- Control local login and authentik/OIDC. Settings are saved in the database; - environment variables only fill blank fields as bootstrap defaults. -

-
-
-
- - - - {/* Warnings */} - {(data?.warnings?.length > 0 || wouldLockOut || cantDisableLocal) && ( -
- {wouldLockOut && ( -

- Cannot disable all login methods; at least one must remain enabled. -

- )} - {cantDisableLocal && !wouldLockOut && ( -

- Cannot disable local login without authentik/OIDC configured, enabled, and mapped to an admin group. -

- )} - {oidcEnabledButIncomplete && ( -

- authentik/OIDC needs {missingFields.join(', ')} before it can be enabled. -

- )} - {data?.warnings?.map((w, i) => ( -

{w}

- ))} -
- )} - - {/* Local login toggle */} - -
- set('local_login_enabled', v)} - label="Enable local login" - /> - - {form.local_login_enabled ? 'Enabled' : 'Disabled'} - -
-
- - {/* OIDC / authentik login toggle */} - -
- set('oidc_login_enabled', v)} - label="Enable OIDC login" - /> - - {!oidcConfigured - ? 'Not fully configured' - : form.oidc_login_enabled ? 'Enabled' : 'Disabled'} - -
-
- -
-
- - authentik / OIDC configuration -
- - - set('oidc_provider_name', e.target.value)} - placeholder="authentik" - className="max-w-xs h-8 text-sm" - /> - - - -
- set('oidc_issuer_url', e.target.value)} - placeholder="https://yourURL.com/application/o/bills/.well-known/openid-configuration" - className="max-w-xl h-8 text-sm" - /> -

- Use the authentik provider issuer URL or full discovery URL, for example https://yourURL.com/application/o/bills/.well-known/openid-configuration. -

- {issuerEndpointWarning && ( -

- This looks like an authorization endpoint. In authentik, copy the provider issuer or OpenID Configuration URL. -

- )} -
-
- - - set('oidc_client_id', e.target.value)} - placeholder="authentik client ID" - className="max-w-xl h-8 text-sm" - /> - - - -
-
- setForm(prev => ({ - ...prev, - oidc_client_secret: e.target.value, - oidc_client_secret_clear: e.target.value ? false : prev.oidc_client_secret_clear, - }))} - placeholder="Leave blank to keep existing secret" - className="h-8 text-sm" - /> - - {data?.oidc_client_secret_set && !form.oidc_client_secret_clear ? 'Secret is set' : 'No secret saved'} - -
- -
-
- - -
- -

- Advanced. Keep client_secret_basic unless your authentik provider explicitly requires client_secret_post. -

-
-
- - -
-
- set('oidc_redirect_uri', e.target.value)} - placeholder={defaultOidcRedirectUri()} - className="h-8 text-sm" - /> - -
-

- Add this exact URL to the Redirect URIs allowed by authentik. -

-
-
- - - set('oidc_scopes', e.target.value)} - placeholder="openid email profile groups" - className="max-w-xl h-8 text-sm" - /> - - - -
- set('oidc_admin_group', e.target.value)} - placeholder="e.g. bill-tracker-admins" - className="max-w-sm h-8 text-sm" - /> -

- Only users in this authentik group become app admins. Admin is never granted by default. -

-
-
- - -
-
- set('oidc_auto_provision', v)} - label="Auto-provision users" - /> - - {form.oidc_auto_provision ? 'Enabled' : 'Disabled'} - -
-

- When enabled, valid authentik users are created in this app on first login. -

-
-
- - -
- - - Admin role only via admin group. - -
-
- - {data?.oidc_env_fallback_used && ( -
- One or more blank database fields are currently using environment fallback values. Saving values here takes precedence. -
- )} - - {oidcTest && ( -
- {oidcTest.ok - ? `Configuration test passed for ${oidcTest.issuer || form.oidc_issuer_url}.` - : oidcTest.error || 'Configuration test failed.'} -
- )} -
- -
- - - -
- -
-
- ); -} - -// ─── Users Table ────────────────────────────────────────────────────────────── - -function UsersTable({ users, onRefresh, currentUser }) { - const [resetForms, setResetForms] = useState({}); - const [deleting, setDeleting] = useState(null); - const [resetting, setResetting] = useState(null); - const [roleUpdating, setRoleUpdating] = useState(null); - const [activeUpdating, setActiveUpdating] = useState(null); - - // Delete confirmation dialog - const [deleteTarget, setDeleteTarget] = useState(null); // user object - - const setReset = (id, v) => setResetForms(p => ({ ...p, [id]: { ...(p[id] || {}), ...v } })); - const getForm = (id) => resetForms[id] || { pw: '', open: false }; - - const handleReset = async (user) => { - const form = getForm(user.id); - if (!form.pw || form.pw.length < 8) { toast.error('Password must be at least 8 characters.'); return; } - setResetting(user.id); - try { - await api.resetPassword(user.id, { password: form.pw }); - toast.success(`Password reset for ${user.username}.`); - setReset(user.id, { pw: '', open: false }); - } catch (err) { - toast.error(err.message || 'Failed to reset password.'); - } finally { - setResetting(null); - } - }; - - const handleDelete = async () => { - if (!deleteTarget) return; - setDeleting(deleteTarget.id); - try { - await api.deleteUser(deleteTarget.id); - toast.success(`User ${deleteTarget.username} deleted.`); - setDeleteTarget(null); - onRefresh(); - } catch (err) { - toast.error(err.message || 'Failed to delete user.'); - } finally { - setDeleting(null); - } - }; - - const handleRoleChange = async (user, role) => { - if (user.role === role) return; - setRoleUpdating(user.id); - try { - await api.updateUserRole(user.id, { role }); - toast.success(`${user.username} is now ${role === 'admin' ? 'an admin' : 'a user'}.`); - onRefresh(); - } catch (err) { - toast.error(err.message || 'Failed to update user role.'); - } finally { - setRoleUpdating(null); - } - }; - - const handleActiveChange = async (user, active) => { - setActiveUpdating(user.id); - try { - await api.updateUserActive(user.id, { active }); - toast.success(`${user.username} is now ${active ? 'active' : 'inactive'}.`); - onRefresh(); - } catch (err) { - toast.error(err.message || 'Failed to update user status.'); - } finally { - setActiveUpdating(null); - } - }; - - return ( - <> - - - Users - - -
- - - - - - - - - - - - {(users || []).map(user => { - const form = getForm(user.id); - const isSelf = currentUser?.id === user.id; - return ( - - - - - - - - - ); - })} - {!users?.length && ( - - - - )} - -
UsernameRoleStatusPasswordReset Password -
-
- {user.username} - {user.is_default_admin && default admin} -
-
-
- - {user.role} - - -
-
- - - {user.must_change_password - ? Temporary - : Set - } - - {form.open ? ( -
- setReset(user.id, { pw: e.target.value })} - className="h-8 text-sm w-36" - /> - - -
- ) : ( - - )} -
- {!isSelf && ( - - )} -
No users found.
-
-
-
- - {/* Delete user confirmation */} - { if (!open) setDeleteTarget(null); }}> - - - Delete {deleteTarget?.username}? - - This is permanent in 2026. The user account and all user-owned data will be deleted, including bills, - payments, categories, monthly state, monthly starting amounts, imports, import history, and sessions. - This cannot be undone from BillTracker. - - - - Cancel - - {deleting ? 'Deleting…' : 'Delete User'} - - - - - - ); -} - -// ─── Add User Card ──────────────────────────────────────────────────────────── - -function AddUserCard({ onCreated }) { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - - const handleCreate = async (e) => { - e.preventDefault(); - setError(''); - - if (password.length < 8) { - const msg = 'Password must be at least 8 characters.'; - setError(msg); - toast.error(msg); - return; - } - - setLoading(true); - try { - await api.createUser({ username, password }); - toast.success(`User "${username}" created.`); - setUsername(''); - setPassword(''); - setError(''); - onCreated(); - } catch (err) { - const errorMessage = err.message || 'Failed to create user.'; - setError(errorMessage); - toast.error(errorMessage); - } finally { - setLoading(false); - } - }; - - return ( - - - Add User - - -
-
- - setUsername(e.target.value)} - placeholder="username" - required - /> -
-
- - setPassword(e.target.value)} - placeholder="Password" - required - /> -
-
- {error && ( -
- {error} -
- )} - - - - - ); -} - -// ─── Backup Management Card ────────────────────────────────────────────────── - -function BackupManagementCard() { - const DEFAULT_SETTINGS = { - enabled: false, - frequency: 'daily', - time: '02:00', - retention_count: 14, - last_run_at: null, - next_run_at: null, - last_error: null, - }; - - const [backups, setBackups] = useState([]); - const [settings, setSettings] = useState(DEFAULT_SETTINGS); - const [loading, setLoading] = useState(true); - const [busy, setBusy] = useState(''); - const [restoreTarget, setRestoreTarget] = useState(null); - const [deleteTarget, setDeleteTarget] = useState(null); - - const load = useCallback(async () => { - setLoading(true); - try { - const [backupData, settingsData] = await Promise.all([ - api.adminBackups(), - api.adminBackupSettings(), - ]); - setBackups(backupData.backups || []); - setSettings({ ...DEFAULT_SETTINGS, ...settingsData }); - } catch (err) { - toast.error(err.message || 'Failed to load backups.'); - } finally { - setLoading(false); - } - }, []); // eslint-disable-line react-hooks/exhaustive-deps - - useEffect(() => { load(); }, [load]); - - const latest = backups[0]; - const setSchedule = (key, value) => setSettings(prev => ({ ...prev, [key]: value })); - - async function handleCreate() { - setBusy('create'); - try { - await api.createAdminBackup(); - toast.success('Backup created.'); - await load(); - } catch (err) { - toast.error(err.message || 'Failed to create backup.'); - } finally { - setBusy(''); - } - } - - async function handleDownload(backup) { - setBusy(`download:${backup.id}`); - try { - const { blob, filename } = await api.downloadAdminBackup(backup.id); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename || backup.id; - document.body.appendChild(a); - a.click(); - a.remove(); - URL.revokeObjectURL(url); - } catch (err) { - toast.error(err.message || 'Failed to download backup.'); - } finally { - setBusy(''); - } - } - - async function handleImport(e) { - const file = e.target.files?.[0]; - e.target.value = ''; - if (!file) return; - - setBusy('import'); - try { - await api.importAdminBackup(file); - toast.success('Backup imported.'); - await load(); - } catch (err) { - toast.error(err.message || 'Failed to import backup.'); - } finally { - setBusy(''); - } - } - - async function handleRestore() { - if (!restoreTarget) return; - setBusy(`restore:${restoreTarget.id}`); - try { - const result = await api.restoreAdminBackup(restoreTarget.id); - toast.success(`Database restored. Pre-restore backup: ${result.pre_restore_backup}`); - setRestoreTarget(null); - await load(); - } catch (err) { - toast.error(err.message || 'Failed to restore backup.'); - } finally { - setBusy(''); - } - } - - async function handleDelete() { - if (!deleteTarget) return; - setBusy(`delete:${deleteTarget.id}`); - try { - await api.deleteAdminBackup(deleteTarget.id); - toast.success('Backup deleted.'); - setDeleteTarget(null); - await load(); - } catch (err) { - toast.error(err.message || 'Failed to delete backup.'); - } finally { - setBusy(''); - } - } - - async function handleSaveSettings() { - setBusy('settings'); - try { - const saved = await api.saveAdminBackupSettings({ - enabled: !!settings.enabled, - frequency: settings.frequency, - time: settings.time, - retention_count: parseInt(settings.retention_count, 10) || 14, - }); - setSettings({ ...DEFAULT_SETTINGS, ...saved }); - toast.success('Backup schedule saved.'); - } catch (err) { - toast.error(err.message || 'Failed to save backup schedule.'); - } finally { - setBusy(''); - } - } - - async function handleRunScheduledNow() { - setBusy('run-scheduled'); - try { - await api.runScheduledBackupNow(); - toast.success('Scheduled backup created.'); - await load(); - } catch (err) { - toast.error(err.message || 'Failed to run scheduled backup.'); - } finally { - setBusy(''); - } - } - - if (loading) { - return Loading backups…; - } - - return ( - <> - - -
-
- Database Backups -

- Admin-only SQLite backup, import, download, restore, and schedule controls. -

-
- - {settings.last_error ? 'Attention' : 'Ready'} - -
-
- -
- {[ - ['Managed backups', backups.length], - ['Latest backup', latest ? formatDateTime(latest.modified_at) : '—'], - ['Latest size', latest ? fmtBytes(latest.size_bytes) : '—'], - ['Scheduled', settings.enabled ? `${settings.frequency} ${settings.time}` : 'Disabled'], - ].map(([label, value]) => ( -
-

{label}

-

{value}

-
- ))} -
- - {settings.last_error && ( -
- {settings.last_error} -
- )} - -
- - - -
- -
- - - - - - - - - - - - {backups.map(backup => ( - - - - - - - - - ))} - {!backups.length && ( - - - - )} - -
BackupTypeModifiedSizeChecksum -
{backup.id}{formatDateTime(backup.modified_at)}{fmtBytes(backup.size_bytes)} - {backup.checksum || '—'} - -
- - - -
-
- No managed backups yet. -
-
- -
-
-
- Scheduled Backups -

- Scheduled runs create managed backups and only apply retention to scheduled backups. -

-
- setSchedule('enabled', v)} label="Enable scheduled backups" /> -
- -
-
- - -
-
- - setSchedule('time', e.target.value)} /> -
-
- - setSchedule('retention_count', e.target.value)} - /> -
-
- -
- {formatDateTime(settings.next_run_at)} -
-
-
- -
-
- Last run: {formatDateTime(settings.last_run_at)} -
-
- Last error: {settings.last_error || '—'} -
-
- -
- - -
-
-
-
- - { if (!open) setRestoreTarget(null); }}> - - - Restore this database backup? - - This replaces the live database with {restoreTarget?.id}. - A pre-restore backup will be created first. Run this during a quiet maintenance window. - - - - Cancel - - {busy.startsWith('restore:') ? 'Restoring…' : 'Restore Database'} - - - - - - { if (!open) setDeleteTarget(null); }}> - - - Delete this backup? - - This permanently deletes {deleteTarget?.id}. - The live database is not affected. - - - - Cancel - - {busy.startsWith('delete:') ? 'Deleting…' : 'Delete Backup'} - - - - - - ); -} - -// ─── CleanupPanel ───────────────────────────────────────────────────────────── - -function CleanupPanel() { - const [status, setStatus] = useState(null); - const [form, setForm] = useState({ - import_sessions_enabled: true, - temp_exports_enabled: true, - temp_export_max_age_hours: 2, - backup_partials_enabled: true, - import_history_enabled: false, - import_history_max_age_days: 365, - }); - const [saving, setSaving] = useState(false); - const [running, setRunning] = useState(false); - const [loading, setLoading] = useState(true); - - const load = useCallback(async () => { - try { - const data = await api.adminCleanup(); - setStatus(data); - if (data.settings) setForm(data.settings); - } catch (err) { - toast.error(err.message || 'Failed to load cleanup settings.'); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { load(); }, [load]); - - const set = (k, v) => setForm(prev => ({ ...prev, [k]: v })); - - async function handleSave() { - setSaving(true); - try { - const next = await api.saveAdminCleanup(form); - if (next) setForm(next); - toast.success('Cleanup settings saved.'); - } catch (err) { - toast.error(err.message || 'Failed to save cleanup settings.'); - } finally { - setSaving(false); - } - } - - async function handleRunNow() { - setRunning(true); - try { - const result = await api.runAdminCleanup(); - setStatus(prev => ({ - ...prev, - last_run_at: result.ran_at, - last_result: result.tasks, - })); - toast.success('Cleanup tasks completed.'); - } catch (err) { - toast.error(err.message || 'Cleanup run failed.'); - } finally { - setRunning(false); - } - } - - function resultLine(label, task, countKey) { - if (!task || task[countKey] == null) return null; - return `${label}: ${task[countKey]}`; - } - - const resultLines = status?.last_result ? [ - resultLine('Import sessions pruned', status.last_result.import_sessions, 'pruned'), - resultLine('Temp export files removed', status.last_result.temp_export_files, 'removed'), - resultLine('Backup partials removed', status.last_result.backup_partials, 'removed'), - resultLine('Import history rows pruned', status.last_result.import_history, 'pruned'), - ].filter(Boolean) : []; - - if (loading) { - return ( - - - Loading cleanup settings… - - - ); - } - - return ( - - -
-
- -
- Cleanup / Maintenance -

- Automatic daily cleanup: expired import sessions, stale export temp files, orphaned backup partials. Runs at 6:00 AM. -

-
-
- Auto -
-
- - - - {/* Last run summary */} -
-
-

Last Run

-

{status?.last_run_at ? formatDateTime(status.last_run_at) : '—'}

-
-
-

Last Result

- {resultLines.length > 0 ? ( -
    - {resultLines.map(line => ( -
  • {line}
  • - ))} -
- ) : ( -

No runs recorded yet

- )} -
-
- - {/* Settings */} -
-

Task Settings

- - {[ - ['import_sessions_enabled', 'Prune expired import sessions (24h TTL)'], - ['temp_exports_enabled', 'Remove stale SQLite export temp files'], - ['backup_partials_enabled', 'Remove orphaned backup .partial / .upload files'], - ].map(([key, label]) => ( - - set(key, v)} label={label} /> - - ))} - - - set('temp_export_max_age_hours', parseInt(e.target.value, 10) || 2)} - disabled={!form.temp_exports_enabled} - className="max-w-[120px] h-8 text-sm" - /> - - - - set('import_history_enabled', v)} label="Trim import history rows" /> - - - {form.import_history_enabled && ( - <> - - set('import_history_max_age_days', parseInt(e.target.value, 10) || 365)} - className="max-w-[120px] h-8 text-sm" - /> - -
- Warning: Import history trimming permanently deletes audit records older than {form.import_history_max_age_days} days. This cannot be undone. -
- - )} -
- - {/* Action buttons */} -
- - -
- -
-
- ); -} - -// ─── AdminPage ──────────────────────────────────────────────────────────────── +import OnboardingWizard from '@/components/admin/OnboardingWizard'; +import EmailNotifCard from '@/components/admin/EmailNotifCard'; +import LoginModeCard from '@/components/admin/LoginModeCard'; +import AuthMethodsCard from '@/components/admin/AuthMethodsCard'; +import UsersTable from '@/components/admin/UsersTable'; +import AddUserCard from '@/components/admin/AddUserCard'; +import BackupManagementCard from '@/components/admin/BackupManagementCard'; +import CleanupPanel from '@/components/admin/CleanupPanel'; export default function AdminPage() { const navigate = useNavigate(); const [me, setMe] = useState(null); - const [hasUsers, setHasUsers] = useState(null); // null = loading + const [hasUsers, setHasUsers] = useState(null); const [users, setUsers] = useState([]); const loadMe = useCallback(async () => { @@ -1849,14 +27,6 @@ export default function AdminPage() { } }, [navigate]); - const loadHasUsers = useCallback(async () => { - try { - const d = await api.hasUsers(); - setHasUsers(d.has_users); - if (d.has_users) loadUsers(); - } catch {} - }, []); // eslint-disable-line react-hooks/exhaustive-deps - const loadUsers = useCallback(async () => { try { const d = await api.adminUsers(); @@ -1864,6 +34,14 @@ export default function AdminPage() { } catch {} }, []); + const loadHasUsers = useCallback(async () => { + try { + const d = await api.hasUsers(); + setHasUsers(d.has_users); + if (d.has_users) loadUsers(); + } catch {} + }, [loadUsers]); + useEffect(() => { loadMe(); loadHasUsers(); @@ -1874,7 +52,6 @@ export default function AdminPage() { loadUsers(); }; - // Loading state if (hasUsers === null) { return (
@@ -1887,7 +64,6 @@ export default function AdminPage() {
- {/* Content */} {!hasUsers ? ( ) : ( diff --git a/client/pages/DataPage.jsx b/client/pages/DataPage.jsx index 7e43ff8..4b96676 100644 --- a/client/pages/DataPage.jsx +++ b/client/pages/DataPage.jsx @@ -1,3084 +1,12 @@ -import React, { useState, useEffect, useRef, useMemo } from 'react'; -import { toast } from 'sonner'; -import { - Upload, FileSpreadsheet, Database, Download, CheckCircle2, XCircle, - AlertTriangle, Loader2, RefreshCw, Clock, ChevronDown, - ChevronUp, SkipForward, Plus, CheckCheck, Sparkles, - List, Building2, ChevronLeft, FileText, Link2, Link2Off, - EyeOff, Eye, Search, -} from 'lucide-react'; +import React, { useState, useEffect } from 'react'; import { api } from '@/api'; -import { cn } from '@/lib/utils'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Switch } from '@/components/ui/switch'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from '@/components/ui/alert-dialog'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; - -// ─── User export availability flag ─────────────────────────────────────────── -// Flip to true when GET /api/export/user-db and GET /api/export/user-excel exist. -const USER_EXPORTS_AVAILABLE = true; - -// ─── Utilities ──────────────────────────────────────────────────────────────── - -function fmt(isoStr) { - if (!isoStr) return '—'; - const d = new Date(isoStr); - return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) - + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); -} - -function groupRowsBySheet(rows) { - const map = new Map(); - for (const row of rows) { - const key = row.sheet_name || '(unknown sheet)'; - if (!map.has(key)) map.set(key, []); - map.get(key).push(row); - } - return Array.from(map.entries()).map(([name, rows]) => ({ name, rows })); -} - -function initialDecisionFromRecommendation(row) { - const rec = row.recommendation || {}; - const action = rec.action === 'ambiguous' ? null : (rec.action || row.proposed_action || null); - - if (!action || row.requires_user_decision) return { action: null }; - if (action === 'skip_row') return { action: 'skip_row' }; - if (action === 'match_existing_bill') { - return { - action, - bill_id: rec.bill_id ?? row.possible_bill_matches?.[0]?.bill_id ?? null, - bill_name: null, - due_day: rec.due_day ?? null, - actual_amount: rec.actual_amount ?? row.detected_amount ?? null, - payment_amount: rec.payment_amount ?? row.detected_payment_amount ?? null, - payment_date: rec.payment_date ?? row.detected_paid_date ?? null, - notes: row.detected_notes ?? null, - }; - } - if (action === 'create_new_bill') { - return { - action, - bill_id: null, - bill_name: rec.bill_name || row.detected_bill_name || '', - category_id: rec.category_id ?? null, - due_day: rec.due_day ?? null, - expected_amount: rec.expected_amount ?? row.detected_amount ?? null, - actual_amount: rec.actual_amount ?? row.detected_amount ?? null, - payment_amount: rec.payment_amount ?? row.detected_payment_amount ?? null, - payment_date: rec.payment_date ?? row.detected_paid_date ?? null, - notes: row.detected_notes ?? null, - }; - } - return { action }; -} - -function safeRawBillName(row) { - const raw = row.raw_values?.find((v) => { - const text = String(v || '').trim(); - if (!text || text.length > 80) return false; - if (/^(?:total|subtotal|sum|grand\s*total)$/i.test(text)) return false; - if (/^\$?\(?\d[\d,]*(?:\.\d{1,2})?\)?$/.test(text)) return false; - if (/^\d{1,2}\/\d{1,2}(?:\/\d{2,4})?$/.test(text)) return false; - if (/^\d{4}-\d{2}-\d{2}$/.test(text)) return false; - return true; - }); - return raw ? String(raw).trim() : ''; -} - -function buildCreateNewDecision(row, currentDecision = {}) { - const rec = row.recommendation || {}; - const billName = currentDecision.bill_name - || row.detected_bill_name - || rec.bill_name - || safeRawBillName(row); - - return { - ...currentDecision, - action: 'create_new_bill', - previous_match_bill_id: currentDecision.bill_id ?? currentDecision.previous_match_bill_id ?? rec.bill_id ?? row.possible_bill_matches?.[0]?.bill_id ?? null, - bill_id: null, - bill_name: billName, - category_id: currentDecision.category_id ?? rec.category_id ?? null, - due_day: currentDecision.due_day ?? rec.due_day ?? null, - expected_amount: currentDecision.expected_amount ?? rec.expected_amount ?? row.detected_amount ?? null, - actual_amount: currentDecision.actual_amount ?? rec.actual_amount ?? row.detected_amount ?? null, - payment_amount: currentDecision.payment_amount ?? rec.payment_amount ?? row.detected_payment_amount ?? null, - payment_date: currentDecision.payment_date ?? rec.payment_date ?? row.detected_paid_date ?? null, - notes: currentDecision.notes ?? row.detected_notes ?? null, - }; -} - -function buildInitialDecisions(rows) { - const d = {}; - for (const row of rows) { - const hasError = row.errors?.length > 0; - if (hasError || row.proposed_action === 'skip_row') { - d[row.row_id] = { action: 'skip_row' }; - } else { - d[row.row_id] = initialDecisionFromRecommendation(row); - } - } - return d; -} - -function isDecisionComplete(action, decision) { - if (!action) return false; - if (action === 'skip_row') return true; - if (action === 'create_new_bill') return !!(decision?.bill_name?.trim()); - if (['match_existing_bill', 'update_monthly_state', 'add_monthly_note', 'create_payment'].includes(action)) { - return !!decision?.bill_id; - } - return true; -} - -// ─── Badges ─────────────────────────────────────────────────────────────────── - -function SourceBadge({ source }) { - const MAP = { - row_date: 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400', - sheet_name: 'bg-blue-500/15 text-blue-600 dark:text-blue-400', - default: 'bg-amber-500/15 text-amber-600 dark:text-amber-500', - ambiguous: 'bg-red-500/15 text-red-600 dark:text-red-400', - }; - const LABELS = { row_date: 'date cell', sheet_name: 'tab name', default: 'default', ambiguous: 'ambiguous' }; - return ( - - {LABELS[source] ?? source} - - ); -} - -function ConfidenceBadge({ confidence }) { - const MAP = { high: 'text-emerald-600 dark:text-emerald-400', medium: 'text-amber-600 dark:text-amber-500', low: 'text-red-500' }; - return {confidence}; -} - -function actionLabel(action) { - const MAP = { - match_existing_bill: 'Match existing bill', - create_new_bill: 'Create new bill', - skip_row: 'Skip row', - ambiguous: 'Needs decision', - update_monthly_state: 'Update monthly record', - add_monthly_note: 'Add monthly note', - create_payment: 'Record as payment', - }; - return MAP[action] || (action ? action.replace(/_/g, ' ') : 'Needs decision'); -} - -function importErrorState(err, fallback) { - const data = err?.data || {}; - return { - message: err?.message || data.message || data.error || fallback, - error: data.error || fallback, - code: data.code || err?.code || null, - details: Array.isArray(data.details) ? data.details : (Array.isArray(err?.details) ? err.details : []), - error_id: data.error_id || null, - }; -} - -function SheetStatusBadge({ status }) { - const MAP = { - parsed: 'bg-emerald-500/15 text-emerald-600', - parsed_month_only: 'bg-amber-500/15 text-amber-600', - ambiguous: 'bg-orange-500/15 text-orange-600', - skipped: 'bg-muted text-muted-foreground', - }; - const LABELS = { - parsed: 'parsed', parsed_month_only: 'month only', ambiguous: 'ambiguous', skipped: 'skipped', - }; - return ( - - {LABELS[status] ?? status} - - ); -} - -// ─── Shared SectionCard ─────────────────────────────────────────────────────── - -function SectionCard({ title, subtitle, children, className }) { - return ( -
-
-

{title}

- {subtitle &&

{subtitle}

} -
-
{children}
-
- ); -} - -// ─── Section 2: Download My Data ───────────────────────────────────────────── - -function ExportCard({ icon: Icon, title, description, filename, endpoint }) { - const [loading, setLoading] = useState(false); - - const handleDownload = async () => { - setLoading(true); - try { - const res = await fetch(endpoint, { credentials: 'include' }); - if (!res.ok) { - let data = {}; - try { data = await res.json(); } catch {} - throw new Error(data.error || `HTTP ${res.status}`); - } - const disposition = res.headers.get('Content-Disposition'); - const match = disposition?.match(/filename="?([^"]+)"?/i); - const name = match ? match[1] : filename; - const blob = await res.blob(); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; a.download = name; a.click(); - URL.revokeObjectURL(url); - toast.success(`${title} downloaded.`); - } catch (err) { - toast.error(err.message || 'Download failed.'); - } finally { - setLoading(false); - } - }; - - const disabled = !USER_EXPORTS_AVAILABLE || loading; - return ( -
-
-
- -
-
-
-

{title}

- {!USER_EXPORTS_AVAILABLE && ( - - Coming soon - - )} -
-

{description}

-
-
-
- -
-
- ); -} - -export function DownloadMyDataSection() { - return ( - - - -
- -

Exports may contain sensitive account metadata (website URLs, usernames, account info). Store exported files securely and avoid sharing them unencrypted.

-
-
-
-

What's included

-
    - {['Bills','Payments','Categories','Monthly bill state','Monthly starting amounts','History ranges','Notes','Export metadata'].map(i => ( -
  • - {i} -
  • - ))} -
-
-
-

What's not included

-
    - {['Passwords','Sessions','Admin settings','Server configuration',"Other users' data",'Full system backup files'].map(i => ( -
  • - {i} -
  • - ))} -
-
-
-
- ); -} - -function CountPill({ label, value }) { - return ( -
-

{label}

-

{value ?? 0}

-
- ); -} - -// ─── Section 2: Transaction Matching ───────────────────────────────────────── - -const TRANSACTION_FILTERS = [ - { id: 'open', label: 'Open', params: { ignored: 'false' } }, - { id: 'unmatched', label: 'Unmatched', params: { match_status: 'unmatched', ignored: 'false' } }, - { id: 'matched', label: 'Matched', params: { match_status: 'matched', ignored: 'false' } }, - { id: 'ignored', label: 'Ignored', params: { match_status: 'ignored', ignored: 'true' } }, - { id: 'all', label: 'All', params: { ignored: 'all' } }, -]; - -function transactionStatus(tx) { - if (tx?.ignored) return 'ignored'; - return tx?.match_status || 'unmatched'; -} - -function TransactionStatusBadge({ tx }) { - const status = transactionStatus(tx); - const styles = { - matched: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600', - ignored: 'border-muted-foreground/30 bg-muted/40 text-muted-foreground', - unmatched: 'border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400', - }; - - return ( - - {status} - - ); -} - -function formatTransactionAmount(amount, currency = 'USD') { - const value = Math.abs(Number(amount || 0)) / 100; - const sign = Number(amount || 0) < 0 ? '-' : '+'; - return `${sign}${new Intl.NumberFormat(undefined, { - style: 'currency', - currency: currency || 'USD', - }).format(value)}`; -} - -function transactionDate(tx) { - return tx?.posted_date || String(tx?.transacted_at || '').slice(0, 10) || '—'; -} - -function transactionTitle(tx) { - return tx?.payee || tx?.description || tx?.memo || 'Transaction'; -} - -function matchScoreTone(score) { - const value = Number(score) || 0; - if (value >= 80) return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'; - if (value >= 55) return 'border-sky-500/30 bg-sky-500/10 text-sky-600 dark:text-sky-400'; - return 'border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400'; -} - -function SuggestedMatchesPanel({ suggestions, loading, actionId, onAccept, onReject }) { - return ( -
-
-
- - - -
-

Suggested matches

-

{loading ? 'Checking transactions' : `${suggestions.length} ready for review`}

-
-
-
- - {loading ? ( -
- - Finding likely bill matches... -
- ) : suggestions.length === 0 ? ( -
- No suggested matches right now. -
- ) : ( -
- {suggestions.map(suggestion => { - const tx = suggestion.transaction || {}; - const bill = suggestion.bill || {}; - const acceptBusy = actionId === `suggestion-match:${suggestion.id}`; - const rejectBusy = actionId === `suggestion-reject:${suggestion.id}`; - const busy = acceptBusy || rejectBusy; - - return ( -
-
-
-
- - {suggestion.score} - -

{transactionTitle(tx)}

-
-

- {transactionDate(tx)} · {tx.source_label || tx.source_type_label || 'Transaction'} -

-
-

- {formatTransactionAmount(tx.amount, tx.currency)} -

-
- -
- -
-

{bill.name || `Bill ${suggestion.billId}`}

-

- Expected ${Number(bill.expected_amount || 0).toFixed(2)} -

-
-
- - {suggestion.reasons?.length > 0 && ( -
- {suggestion.reasons.slice(0, 4).map(reason => ( - - {reason} - - ))} -
- )} - -
- - -
-
- ); - })} -
- )} -
- ); -} - -function MatchBillDialog({ open, onOpenChange, transaction, bills, onConfirm, loading }) { - const [query, setQuery] = useState(''); - const [selectedBillId, setSelectedBillId] = useState(''); - - useEffect(() => { - if (open) { - setQuery(''); - setSelectedBillId(transaction?.matched_bill_id ? String(transaction.matched_bill_id) : ''); - } - }, [open, transaction?.id, transaction?.matched_bill_id]); - - const filteredBills = useMemo(() => { - const q = query.trim().toLowerCase(); - if (!q) return bills.slice(0, 40); - return bills - .filter(bill => String(bill.name || '').toLowerCase().includes(q)) - .slice(0, 40); - }, [bills, query]); - - const selectedBill = bills.find(bill => String(bill.id) === String(selectedBillId)); - - return ( - - - - Match Transaction - - Choose the bill this transaction paid. Nothing changes until you confirm. - - - - {transaction && ( -
-
-
-

{transactionTitle(transaction)}

-

- {transactionDate(transaction)} · {transaction.source_label || transaction.source_type_label || 'Transaction'} -

-
-

- {formatTransactionAmount(transaction.amount, transaction.currency)} -

-
- {transaction.description && transaction.description !== transactionTitle(transaction) && ( -

{transaction.description}

- )} -
- )} - -
- - -
- {filteredBills.length === 0 ? ( -

No bills found.

- ) : ( -
- {filteredBills.map(bill => ( - - ))} -
- )} -
-
- - - - - -
-
- ); -} - -function TransactionMatchingSection({ refreshKey }) { - const [transactions, setTransactions] = useState([]); - const [suggestions, setSuggestions] = useState([]); - const [bills, setBills] = useState([]); - const [filter, setFilter] = useState('open'); - const [loading, setLoading] = useState(true); - const [suggestionsLoading, setSuggestionsLoading] = useState(true); - const [billsLoading, setBillsLoading] = useState(true); - const [actionId, setActionId] = useState(null); - const [matchOpen, setMatchOpen] = useState(false); - const [matchTransaction, setMatchTransaction] = useState(null); - - const currentFilter = TRANSACTION_FILTERS.find(item => item.id === filter) || TRANSACTION_FILTERS[0]; - - const loadTransactions = async () => { - setLoading(true); - try { - const data = await api.transactions({ limit: 100, ...currentFilter.params }); - setTransactions(data || []); - } catch (err) { - toast.error(err.message || 'Failed to load transactions.'); - setTransactions([]); - } finally { - setLoading(false); - } - }; - - const loadSuggestions = async () => { - setSuggestionsLoading(true); - try { - const data = await api.matchSuggestions({ limit: 8 }); - setSuggestions(data || []); - } catch (err) { - toast.error(err.message || 'Failed to load match suggestions.'); - setSuggestions([]); - } finally { - setSuggestionsLoading(false); - } - }; - - const refreshTransactionWorkbench = async () => { - await Promise.all([loadTransactions(), loadSuggestions()]); - }; - - const loadBills = async () => { - setBillsLoading(true); - try { - const data = await api.bills(); - setBills(data || []); - } catch { - setBills([]); - } finally { - setBillsLoading(false); - } - }; - - useEffect(() => { loadBills(); }, []); - useEffect(() => { loadTransactions(); }, [filter, refreshKey]); - useEffect(() => { loadSuggestions(); }, [refreshKey]); - - const openMatchDialog = (tx) => { - setMatchTransaction(tx); - setMatchOpen(true); - if (!bills.length && !billsLoading) loadBills(); - }; - - const runTransactionAction = async (tx, action) => { - setActionId(`${action}:${tx.id}`); - try { - if (action === 'unmatch') { - await api.unmatchTransaction(tx.id); - toast.success('Transaction unmatched.'); - } else if (action === 'ignore') { - await api.ignoreTransaction(tx.id); - toast.success('Transaction ignored.'); - } else if (action === 'unignore') { - await api.unignoreTransaction(tx.id); - toast.success('Transaction restored.'); - } - await refreshTransactionWorkbench(); - } catch (err) { - toast.error(err.message || 'Transaction action failed.'); - } finally { - setActionId(null); - } - }; - - const confirmMatch = async (billId) => { - if (!matchTransaction) return; - setActionId(`match:${matchTransaction.id}`); - try { - await api.matchTransaction(matchTransaction.id, billId); - toast.success('Transaction matched to bill.'); - setMatchOpen(false); - setMatchTransaction(null); - await refreshTransactionWorkbench(); - } catch (err) { - toast.error(err.message || 'Transaction match failed.'); - } finally { - setActionId(null); - } - }; - - const acceptSuggestion = async (suggestion) => { - setActionId(`suggestion-match:${suggestion.id}`); - try { - await api.matchTransaction(suggestion.transactionId, suggestion.billId); - toast.success('Suggested match confirmed.'); - await refreshTransactionWorkbench(); - } catch (err) { - toast.error(err.message || 'Suggested match failed.'); - } finally { - setActionId(null); - } - }; - - const rejectSuggestion = async (suggestion) => { - setActionId(`suggestion-reject:${suggestion.id}`); - try { - await api.rejectMatchSuggestion(suggestion.id); - toast.success('Suggestion rejected.'); - await loadSuggestions(); - } catch (err) { - toast.error(err.message || 'Suggestion could not be rejected.'); - } finally { - setActionId(null); - } - }; - - return ( - -
-
-
- {TRANSACTION_FILTERS.map(item => ( - - ))} -
- -
- - - -
- {loading ? ( -
Loading transactions…
- ) : transactions.length === 0 ? ( -
- No transactions found for this filter. -
- ) : ( - - - - - - - - - - - - {transactions.map(tx => { - const status = transactionStatus(tx); - const busy = actionId?.endsWith(`:${tx.id}`); - return ( - - - - - - - - ); - })} - -
DateTransactionMatchAmountActions
- {transactionDate(tx)} - -
-

{transactionTitle(tx)}

-

- {[tx.description, tx.account_name, tx.source_label].filter(Boolean).join(' · ') || '—'} -

-
-
-
- - {tx.matched_bill_name ? ( - {tx.matched_bill_name} - ) : ( - No bill linked - )} -
-
- {formatTransactionAmount(tx.amount, tx.currency)} - -
- {status === 'ignored' ? ( - - ) : ( - <> - {status === 'matched' ? ( - - ) : ( - - )} - - - )} -
-
- )} -
-
- - -
- ); -} - -// ─── Section 1: Import Transaction CSV ─────────────────────────────────────── - -const CSV_MAPPING_FIELDS = [ - 'posted_date', - 'amount', - 'debit_amount', - 'credit_amount', - 'description', - 'payee', - 'memo', - 'category', - 'account', - 'transaction_id', - 'transaction_type', - 'currency', - 'transacted_at', -]; - -function compactMapping(mapping) { - return Object.fromEntries( - Object.entries(mapping || {}).filter(([, value]) => value), - ); -} - -function canCommitCsvMapping(mapping) { - return !!mapping?.posted_date && !!(mapping.amount || mapping.debit_amount || mapping.credit_amount); -} - -const CSV_IMPORT_STEPS = ['Upload', 'Preview', 'Map', 'Commit', 'Results']; - -function csvImportStepIndex(preview, mapping, commitState) { - if (commitState.status === 'done') return 4; - if (commitState.status === 'loading') return 3; - if (preview.status === 'ready') return canCommitCsvMapping(mapping) ? 3 : 2; - if (preview.status === 'loading' || preview.status === 'error') return 1; - return 0; -} - -function CsvImportStepper({ activeIndex }) { - return ( -
- {CSV_IMPORT_STEPS.map((step, index) => { - const complete = index < activeIndex; - const active = index === activeIndex; - return ( -
- - {complete ? : index + 1} - - {step} -
- ); - })} -
- ); -} - -function csvFieldRequirement(field, mapping) { - if (field === 'posted_date') return 'Required'; - if (['amount', 'debit_amount', 'credit_amount'].includes(field)) { - return canCommitCsvMapping({ ...mapping, posted_date: mapping?.posted_date || '__date__' }) - ? 'Amount source' - : 'One required'; - } - return 'Optional'; -} - -function csvFieldSamples(preview, header) { - if (!header) return []; - const values = []; - for (const row of preview?.sampleRows || []) { - const value = String(row?.[header] || '').trim(); - if (value && !values.includes(value)) values.push(value); - if (values.length >= 3) break; - } - return values; -} - -function CsvMappingRow({ field, label, preview, mapping, onChange, disabled = false }) { - const headers = preview?.headers || []; - const suggested = preview?.suggestedMapping?.[field] || ''; - const current = mapping[field] || ''; - const used = new Set(Object.entries(mapping) - .filter(([key, value]) => key !== field && value) - .map(([, value]) => value)); - const requirement = csvFieldRequirement(field, mapping); - const missingRequired = (requirement === 'Required' || requirement === 'One required') && !current; - const samples = csvFieldSamples(preview, current); - const suggestedAvailable = suggested && suggested !== current && !used.has(suggested); - - return ( -
-
-
-

{label}

- - {requirement} - -
-

{field}

-
- -
- -
- {current && current === suggested && ( - Suggested match - )} - {suggestedAvailable && !disabled && ( - - )} - {missingRequired && ( - Needs a column - )} -
-
- -
- {samples.length > 0 ? ( -
- {samples.map(value => ( - - {value} - - ))} -
- ) : ( -

- {current ? 'No sample values' : 'Map a column to preview values'} -

- )} -
-
- ); -} - -function CsvMappingReview({ preview, fields, mapping, onMappingChange, onUseSuggested, onClearMapping, disabled = false }) { - const mappingFields = CSV_MAPPING_FIELDS.filter(field => fields[field]); - const mappedCount = mappingFields.filter(field => mapping[field]).length; - const hasSuggestedMapping = Object.values(preview?.suggestedMapping || {}).some(Boolean); - const missingRequired = [ - !mapping.posted_date ? 'Posted date' : null, - !(mapping.amount || mapping.debit_amount || mapping.credit_amount) ? 'Amount' : null, - ].filter(Boolean); - - return ( -
-
-
-

Column mapping

-

- {mappedCount} of {mappingFields.length} fields mapped - {missingRequired.length > 0 && ` · missing ${missingRequired.join(' and ')}`} -

-
-
- - -
-
- -
- Field - CSV Column - Sample Values -
- -
- {mappingFields.map(field => ( - - ))} -
-
- ); -} - -function CsvSampleTable({ preview }) { - const headers = preview?.headers || []; - const sampleRows = preview?.sampleRows || []; - const visibleHeaders = headers.slice(0, 8); - const hiddenCount = Math.max(0, headers.length - visibleHeaders.length); - - if (sampleRows.length === 0) { - return

No sample rows found.

; - } - - return ( -
- - - - {visibleHeaders.map(header => ( - - ))} - {hiddenCount > 0 && ( - - )} - - - - {sampleRows.map((row, index) => ( - - {visibleHeaders.map(header => ( - - ))} - {hiddenCount > 0 && ( - - )} - - ))} - -
{header}+{hiddenCount}
- {row[header] || '—'} - more columns
-
- ); -} - -function formatCsvRowDetail(detail) { - if (!detail) return ''; - const field = detail.field ? `${detail.field}: ` : ''; - return `${field}${detail.message || detail.value || JSON.stringify(detail)}`; -} - -export function ImportTransactionCsvSection({ onHistoryRefresh }) { - const fileRef = useRef(null); - const [file, setFile] = useState(null); - const [preview, setPreview] = useState({ status: 'idle', data: null, error: null }); - const [mapping, setMapping] = useState({}); - const [commitState, setCommitState] = useState({ status: 'idle', result: null, error: null }); - - const reset = () => { - setFile(null); - setPreview({ status: 'idle', data: null, error: null }); - setMapping({}); - setCommitState({ status: 'idle', result: null, error: null }); - if (fileRef.current) fileRef.current.value = ''; - }; - - const handleMappingChange = (field, header) => { - if (commitState.status === 'done') return; - setMapping(prev => { - const next = { ...prev }; - if (header) next[field] = header; - else delete next[field]; - return next; - }); - setCommitState({ status: 'idle', result: null, error: null }); - }; - - const handlePreview = async () => { - if (!file) { - toast.error('Choose a CSV file first.'); - return; - } - setPreview({ status: 'loading', data: null, error: null }); - setMapping({}); - setCommitState({ status: 'idle', result: null, error: null }); - try { - const data = await api.previewCsvTransactionImport(file); - setPreview({ status: 'ready', data, error: null }); - setMapping(compactMapping(data.suggestedMapping || {})); - toast.success('CSV preview ready.'); - } catch (err) { - const errorState = importErrorState(err, 'CSV preview failed.'); - setPreview({ status: 'error', data: null, error: errorState }); - toast.error(errorState.message || 'CSV preview failed.'); - } - }; - - const handleCommit = async () => { - if (!preview.data?.import_session_id || !canCommitCsvMapping(mapping)) return; - setCommitState({ status: 'loading', result: null, error: null }); - try { - const result = await api.commitCsvTransactionImport({ - import_session_id: preview.data.import_session_id, - mapping: compactMapping(mapping), - }); - setCommitState({ status: 'done', result, error: null }); - toast.success(`CSV imported — ${result.imported} imported, ${result.skipped} skipped.`); - onHistoryRefresh?.(); - } catch (err) { - const errorState = importErrorState(err, 'CSV import failed.'); - setCommitState({ status: 'error', result: null, error: errorState }); - toast.error(errorState.message || 'CSV import failed.'); - } - }; - - const applySuggestedMapping = () => { - if (commitState.status === 'done') return; - setMapping(compactMapping(preview.data?.suggestedMapping || {})); - setCommitState({ status: 'idle', result: null, error: null }); - }; - - const clearMapping = () => { - if (commitState.status === 'done') return; - setMapping({}); - setCommitState({ status: 'idle', result: null, error: null }); - }; - - const fields = preview.data?.fields || {}; - const canCommit = preview.status === 'ready' - && preview.data?.import_session_id - && canCommitCsvMapping(mapping) - && commitState.status !== 'loading' - && commitState.status !== 'done'; - const activeStep = csvImportStepIndex(preview, mapping, commitState); - const failedRows = (commitState.result?.details || []).filter(d => d.result === 'failed'); - const skippedRows = (commitState.result?.details || []).filter(d => d.result === 'skipped_duplicate'); - - return ( - -
-
-
- -
-

Import transaction rows from CSV.

-

- This importer creates shared transaction records only. It does not match transactions to bills yet. -

-
-
-
- - - -
-
- -
- - -
-
-
- - {preview.status === 'error' && ( -
- - {preview.error?.message || 'CSV preview failed.'} - {preview.error?.details?.length > 0 && ( -
    - {preview.error.details.map((d, i) => ( -
  • {d.message || JSON.stringify(d)}
  • - ))} -
- )} -
- )} - - {preview.status === 'ready' && preview.data && ( -
-
-
-
-

CSV Preview

-

{file?.name || 'Transaction CSV'}

-
-
- - - -
-
- - {preview.data.errors?.length > 0 && ( -
-

Review mapping

-
    - {preview.data.errors.map((issue, i) => ( -
  • - - {issue.message || JSON.stringify(issue)} -
  • - ))} -
-
- )} - - -
- - - -
-
-

- {canCommitCsvMapping(mapping) ? 'Ready to commit' : 'Mapping incomplete'} -

-

- Duplicates are skipped using a CSV transaction ID when available, otherwise a stable row hash. -

-
- {commitState.status === 'done' ? ( - - ) : ( - - )} -
-
- )} - - {commitState.status === 'done' && commitState.result && ( -
-
- -

CSV transaction import complete

-
-
- - - -
- {skippedRows.length > 0 && ( -
-

Skipped duplicates ({skippedRows.length})

-
    - {skippedRows.map(row => ( -
  • - Row {row.row}: {row.provider_transaction_id} -
  • - ))} -
-
- )} - {failedRows.length > 0 && ( -
-

Failed rows ({failedRows.length})

-
    - {failedRows.map((row, index) => ( -
  • -

    Row {row.row}: {row.message}

    - {row.details?.length > 0 && ( -
      - {row.details.map((detail, detailIndex) => ( -
    • {formatCsvRowDetail(detail)}
    • - ))} -
    - )} -
  • - ))} -
-
- )} -
- )} - - {commitState.status === 'error' && ( -
- - {commitState.error?.message || 'CSV import failed.'} - {commitState.error?.details?.length > 0 && ( -
    - {commitState.error.details.map((d, i) => ( -
  • {d.message || JSON.stringify(d)}
  • - ))} -
- )} -
- )} -
-
- ); -} - -// ─── Section 3: Import My Data Export ──────────────────────────────────────── - -export function ImportMyDataSection({ onHistoryRefresh }) { - const fileRef = useRef(null); - const [file, setFile] = useState(null); - const [preview, setPreview] = useState({ status: 'idle', data: null, error: null }); - const [applyState, setApplyState] = useState({ status: 'idle', result: null, error: null }); - const [confirmOpen, setConfirmOpen] = useState(false); - - const reset = () => { - setFile(null); - setPreview({ status: 'idle', data: null, error: null }); - setApplyState({ status: 'idle', result: null, error: null }); - if (fileRef.current) fileRef.current.value = ''; - }; - - const handlePreview = async () => { - if (!file) { - toast.error('Choose a SQLite data export first.'); - return; - } - setPreview({ status: 'loading', data: null, error: null }); - setApplyState({ status: 'idle', result: null, error: null }); - try { - const data = await api.previewUserDbImport(file); - setPreview({ status: 'ready', data, error: null }); - toast.success('SQLite export preview ready.'); - } catch (err) { - setPreview({ status: 'error', data: null, error: importErrorState(err, 'SQLite import preview failed.') }); - toast.error(err.message || 'SQLite import preview failed.'); - } - }; - - const handleApply = () => { - if (!preview.data?.import_session_id) return; - setConfirmOpen(true); - }; - - const handleConfirmImport = async () => { - setConfirmOpen(false); - setApplyState({ status: 'loading', result: null, error: null }); - try { - const result = await api.applyUserDbImport({ - import_session_id: preview.data.import_session_id, - options: { overwrite: false }, - }); - setApplyState({ status: 'done', result, error: null }); - toast.success('SQLite data import applied.'); - onHistoryRefresh?.(); - } catch (err) { - setApplyState({ status: 'error', result: null, error: importErrorState(err, 'SQLite import apply failed.') }); - toast.error(err.message || 'SQLite import apply failed.'); - } - }; - - const counts = preview.data?.counts || {}; - const summary = preview.data?.summary || {}; - - return ( - <> - -
-
-
- -
-

Import a SQLite data export created by this app.

-

- This is not a full system restore. Existing records are skipped by default, and admin/system data is never imported. -

-
-
-
- -
- -
- - -
-
- - {preview.status === 'error' && ( -
- - {preview.error?.message || 'SQLite import preview failed.'} - {preview.error?.details?.length > 0 && ( -
    - {preview.error.details.map((d, i) => ( -
  • {d.message || d.table || JSON.stringify(d)}
  • - ))} -
- )} -
- )} - - {preview.status === 'ready' && preview.data && ( -
-
-
-
-

Preview ready

-

- Exported {fmt(preview.data.metadata?.exported_at)} · {preview.data.source_filename || 'SQLite export'} -

-
- - User data only - -
-
- - - - - -
-
- {Object.entries(summary).filter(([, v]) => v && typeof v === 'object').map(([key, value]) => ( -
-

{key.replace(/_/g, ' ')}

-

- create {value.create || 0} · skip {value.skip || 0} · conflict {value.conflict || 0} -

-
- ))} -
- {preview.data.warnings?.length > 0 && ( -
- {preview.data.warnings.map((warning, i) => ( -

- {warning} -

- ))} -
- )} -
- -
-

Review the preview before applying. Nothing is imported until you confirm.

- -
-
- )} - - {applyState.status === 'done' && applyState.result && ( -
-

SQLite import applied

-
- - - - -
-
- )} - - {applyState.status === 'error' && ( -
- {applyState.error?.message || 'SQLite import apply failed.'} -
- )} -
-
- {/* Import confirmation dialog */} - - - - Import SQLite data export? - - Import this SQLite data export into your account? Existing records will be skipped by default. - - - - Cancel - - Confirm Import - - - - - - ); -} - -// ─── Section 4: Import History ──────────────────────────────────────────────── - -export function ImportHistorySection({ history, loading, onRefresh }) { - if (loading) { - return ( - -
Loading…
-
- ); - } - - const rows = history ?? []; - - return ( - -
-

- {rows.length === 0 ? 'No imports yet.' : `${rows.length} import${rows.length === 1 ? '' : 's'}`} -

- -
- {rows.length > 0 && ( -
- - - - {['Date','File','Sheet','Parsed','Created','Updated','Skipped','Errors'].map(h => ( - - ))} - - - - {rows.map(r => ( - - - - - - - - - - - ))} - -
{h}
- {fmt(r.imported_at)} - {r.source_filename || '—'}{r.sheet_name || '—'}{r.rows_parsed}{r.rows_created}{r.rows_updated}{r.rows_skipped}{r.rows_errored}
-
- )} -
- ); -} - -// ─── XLSX Import: Workbook Summary ──────────────────────────────────────────── - -function WorkbookSummaryCard({ workbook }) { - const isMulti = workbook.parse_mode === 'all_sheets'; - - return ( -
-
-

Workbook Summary

- - {isMulti ? `${workbook.total_row_count} rows across ${workbook.sheet_names.length} tabs` : `${workbook.row_count} rows · ${workbook.selected_sheet}`} - -
- {isMulti && workbook.sheets?.length > 0 && ( -
- {workbook.sheets.map(s => ( -
- {s.name} -
- {s.detected_year && s.detected_month && ( - - {String(s.detected_month).padStart(2,'0')}/{s.detected_year} - - )} - - {s.status !== 'skipped' && {s.row_count} rows} -
-
- ))} -
- )} -
- ); -} - -// ─── XLSX Import: Row Decision Controls ────────────────────────────────────── - -const ACTIONS_NEEDING_BILL = new Set(['match_existing_bill','update_monthly_state','add_monthly_note','create_payment']); - -function RowDecisionRow({ row, decision, onDecisionChange, allBills, categories, selected, onSelectedChange }) { - const [expanded, setExpanded] = useState(row.requires_user_decision || !decision?.action); - - const action = decision?.action ?? null; - const isSkip = action === 'skip_row'; - const hasError = row.errors?.length > 0; - const complete = isDecisionComplete(action, decision); - const rec = row.recommendation || {}; - - const suggestedBills = row.possible_bill_matches ?? []; - const suggestedIds = new Set(suggestedBills.map(b => b.bill_id)); - const otherBills = allBills.filter(b => !suggestedIds.has(b.id)); - - const handleAction = (val) => { - const next = { ...decision, action: val }; - if (val === 'create_new_bill') { - Object.assign(next, buildCreateNewDecision(row, decision)); - } else if (ACTIONS_NEEDING_BILL.has(val)) { - next.bill_name = null; - next.bill_id = decision?.bill_id ?? decision?.previous_match_bill_id ?? rec.bill_id ?? suggestedBills[0]?.bill_id ?? null; - next.actual_amount = decision?.actual_amount ?? rec.actual_amount ?? row.detected_amount ?? null; - next.payment_amount = decision?.payment_amount ?? rec.payment_amount ?? row.detected_payment_amount ?? null; - next.payment_date = decision?.payment_date ?? rec.payment_date ?? row.detected_paid_date ?? null; - } else { - next.bill_id = null; - next.bill_name = null; - } - onDecisionChange(row.row_id, next); - if (val === 'skip_row') setExpanded(false); - }; - - const handleBill = (e) => { - onDecisionChange(row.row_id, { ...decision, bill_id: parseInt(e.target.value, 10) || null }); - }; - - const handleBillName = (e) => { - onDecisionChange(row.row_id, { ...decision, bill_name: e.target.value }); - }; - - const handleDecisionField = (field, value) => { - onDecisionChange(row.row_id, { ...decision, [field]: value }); - }; - - return ( -
- {/* Main row */} -
setExpanded(e => !e)} - > - {/* Selection */} -
e.stopPropagation()}> - onSelectedChange(row.row_id, e.target.checked)} - aria-label={`Select row ${row.source_row_number}`} - className="h-4 w-4 rounded border-border accent-primary" - /> -
- - {/* Status icon */} -
- {hasError ? : - isSkip ? : - complete ? : - action !== null ? : - } -
- - {/* Content */} -
-
- #{row.source_row_number} - {row.sheet_name && {row.sheet_name}} - {row.detected_year && row.detected_month && ( - - {String(row.detected_month).padStart(2,'0')}/{row.detected_year} - - )} - {row.year_month_source && } -
-
- - {row.detected_bill_name || '(no bill name)'} - - {row.detected_amount != null && ( - - ${row.detected_amount.toFixed(2)} - - )} - {row.detected_paid_date && ( - - paid {row.detected_paid_date} - - )} - {row.detected_labels?.length > 0 && ( - {row.detected_labels.join(', ')} - )} - {row.detected_notes && ( - {row.detected_notes} - )} -
-
- - {/* Right: action status + expand */} -
- {action === null ? ( - Needs decision - ) : isSkip ? ( - Skipped - ) : ( - {action.replace(/_/g,' ')} - )} - {action !== 'skip_row' && ( - expanded ? : - )} -
-
- - {/* Expanded decision controls */} - {expanded && !hasError && ( -
- {/* Recommendation */} - {rec.action && ( -
-
- Recommended: {actionLabel(rec.action)} - {rec.bill_name && rec.action === 'match_existing_bill' && ( - → {rec.bill_name} - )} - {rec.category_name && ( - Category: {rec.category_name} - )} - {rec.due_day && Due day: {rec.due_day}} - {rec.actual_amount != null && ${Number(rec.actual_amount).toFixed(2)}} - -
- {rec.reason &&

Reason: {rec.reason}

} -
- )} - - {/* Warnings */} - {(rec.warnings?.length > 0 || row.warnings?.length > 0) && ( -
- {Array.from(new Set([...(rec.warnings || []), ...(row.warnings || [])])).map((w, i) => ( -

- {w} -

- ))} -
- )} - - {/* Possible matches hint */} - {suggestedBills.length > 0 && ( -
- Suggested: - {suggestedBills.slice(0, 3).map(b => ( - - ))} -
- )} - - {/* Action selector */} -
- - -
- - {/* Bill selector (for actions that need a bill) */} - {ACTIONS_NEEDING_BILL.has(action) && ( -
- - -
- )} - - {/* Bill name input for create_new_bill */} - {action === 'create_new_bill' && ( -
-
- - -
-
- - - {rec.category_name && Suggested: {rec.category_name}} -
-
- - handleDecisionField('due_day', e.target.value ? parseInt(e.target.value, 10) : null)} - placeholder="Due day" - className="h-8 text-sm w-24" - /> - handleDecisionField('expected_amount', e.target.value === '' ? null : parseFloat(e.target.value))} - placeholder="Expected amount" - className="h-8 text-sm w-40" - /> -
-
- )} - - {action && action !== 'skip_row' && ( -
- - handleDecisionField('payment_date', e.target.value || null)} - className="h-8 text-sm w-40" - /> - handleDecisionField('payment_amount', e.target.value === '' ? null : parseFloat(e.target.value))} - placeholder="Paid amount" - className="h-8 text-sm w-36" - /> -
- )} - - {/* Quick skip */} - {action !== 'skip_row' && ( - - )} -
- )} -
- ); -} - -// ─── XLSX Import: Preview Table ─────────────────────────────────────────────── - -function PreviewTable({ rows, decisions, onDecisionChange, allBills, categories, selectedRows, onSelectedChange }) { - const groups = groupRowsBySheet(rows); - const multiTab = groups.length > 1; - - return ( -
- {groups.map(({ name, rows: groupRows }) => ( -
- {multiTab && ( -
- - {name} - · {groupRows.length} rows -
- )} - {groupRows.map(row => ( - - ))} -
- ))} -
- ); -} - -function BulkActionBar({ - rows, - selectedRows, - onSelectAll, - onClearSelection, - onBulkSkip, - onBulkCreateNew, - onBulkReset, -}) { - const allSelected = rows.length > 0 && rows.every(r => selectedRows.has(r.row_id)); - const selectedCount = selectedRows.size; - - return ( -
-
- - -
- {selectedCount > 0 && ( - {selectedCount} row{selectedCount === 1 ? '' : 's'} selected - )} - {selectedCount > 0 && ( - <> - - - - - - )} -
-
-
- ); -} - -// ─── Section 1: Import Spreadsheet History ──────────────────────────────────── - -const INITIAL_OPTIONS = { - parseAllSheets: true, - defaultYear: new Date().getFullYear(), - defaultMonth: '', -}; - -// ─── Bill History Import helpers ────────────────────────────────────────────── - -function ConfidenceDot({ level }) { - const cls = level === 'high' ? 'bg-emerald-500' - : level === 'medium' ? 'bg-amber-500' - : 'bg-muted-foreground/30'; - return ; -} - -function useBillGroups(previewRows, allBills) { - return useMemo(() => { - const billMap = new Map(allBills.map(b => [b.id, b])); - const groups = new Map(); - - for (const row of previewRows) { - for (const match of (row.possible_bill_matches ?? [])) { - if (!billMap.has(match.bill_id)) continue; - if (!groups.has(match.bill_id)) { - groups.set(match.bill_id, { - bill: billMap.get(match.bill_id), - rows: [], - counts: { high: 0, medium: 0, low: 0 }, - }); - } - const g = groups.get(match.bill_id); - if (!g.rows.find(r => r.row_id === row.row_id)) { - g.rows.push({ ...row, _match: match }); - g.counts[match.match_confidence] = (g.counts[match.match_confidence] || 0) + 1; - } - } - } - return [...groups.values()].sort((a, b) => - b.rows.length !== a.rows.length ? b.rows.length - a.rows.length : b.counts.high - a.counts.high - ); - }, [previewRows, allBills]); -} - -function rowDateLabel(row) { - if (row.detected_year && row.detected_month) - return `${row.detected_year}-${String(row.detected_month).padStart(2, '0')}`; - return row.detected_paid_date ?? '—'; -} - -function billImportProgress(rows, importResult) { - const completedRowIds = importResult?.completedRowIds ?? new Set(); - const remainingRows = rows.filter(row => !completedRowIds.has(row.row_id)); - return { - completedCount: rows.length - remainingRows.length, - remainingRows, - remainingCount: remainingRows.length, - }; -} - -function detailImportedAnything(detail) { - return ['created', 'updated', 'overwritten', 'imported'].includes(detail?.result) - || detail?.payment === 'created'; -} - -function detailCompletesImport(detail) { - if (!detail?.row_id) return false; - if (['error', 'ambiguous', 'skipped_conflict'].includes(detail.result)) return false; - if (detail.result === 'skipped') return false; - return detailImportedAnything(detail) - || detail.result === 'skipped_duplicate' - || detail.payment === 'skipped_duplicate'; -} - -function BillDetailView({ group, onBack, onImport, isImporting, importResult }) { - const { bill, rows } = group; - const { completedCount, remainingCount } = billImportProgress(rows, importResult); - const sorted = [...rows].sort((a, b) => { - const da = (a.detected_year ?? 0) * 100 + (a.detected_month ?? 0); - const db = (b.detected_year ?? 0) * 100 + (b.detected_month ?? 0); - return da - db; - }); - - return ( -
-
- - {bill.name} -
- {importResult && ( -
- - {completedCount === rows.length - ? 'All imported' - : `${completedCount} imported · ${remainingCount} remaining`} - {importResult.duplicates > 0 - && ` · ${importResult.duplicates} dupes`} - - {importResult.duplicates > 0 && importResult.earliestDup && ( -

- {importResult.duplicates} dup{importResult.duplicates > 1 ? 's' : ''} · recorded{' '} - {importResult.earliestDup.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })} -

- )} -
- )} - -
-
-
- {sorted.map(row => ( -
- - {rowDateLabel(row)} - - {row.detected_amount != null ? `$${Number(row.detected_amount).toFixed(2)}` : '—'} - - {row.detected_name ?? '—'} - - {row._match.match_confidence} - -
- ))} -
-
- ); -} - -function BillHistoryView({ previewRows, allBills, importingBillId, billImportResults, onImportBill }) { - const [selectedBillId, setSelectedBillId] = useState(null); - const billGroups = useBillGroups(previewRows, allBills); - - if (billGroups.length === 0) { - return ( -
- No existing bills matched rows in this file. -
- ); - } - - if (selectedBillId) { - const group = billGroups.find(g => g.bill.id === selectedBillId); - return group - ? setSelectedBillId(null)} - onImport={() => onImportBill(group)} /> - : null; - } - - return ( -
- {billGroups.map(g => { - const { bill, rows, counts } = g; - const isImporting = importingBillId === bill.id; - const importResult = billImportResults.get(bill.id) ?? null; - const { completedCount, remainingCount } = billImportProgress(rows, importResult); - - const sorted3 = [...rows] - .sort((a, b) => { - const da = (a.detected_year ?? 0) * 100 + (a.detected_month ?? 0); - const db = (b.detected_year ?? 0) * 100 + (b.detected_month ?? 0); - return da - db; - }) - .slice(0, 3); - - return ( -
-
-
- {bill.name} - - {rows.length} row{rows.length !== 1 ? 's' : ''} - - {counts.high > 0 && {counts.high} high} - {counts.medium > 0 && {counts.medium} med} - {counts.low > 0 && {counts.low} low} - {importResult && (() => { - const allImported = completedCount === rows.length; - const fmtDate = d => d?.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); - const dupDate = importResult.earliestDup ? ` · ${fmtDate(importResult.earliestDup)}` : ''; - return ( -
- - {allImported ? 'All imported' : `${completedCount} imported · ${remainingCount} remaining`} - {importResult.duplicates > 0 && ` · ${importResult.duplicates} dupes`} - {importResult.errored > 0 && ` · ${importResult.errored} errors`} - - {importResult.duplicates > 0 && ( -

- {importResult.duplicates} duplicate{importResult.duplicates > 1 ? 's' : ''}{dupDate} -

- )} -
- ); - })()} -
-
- {sorted3.map(row => ( -
- - {rowDateLabel(row)} - {row.detected_amount != null && ( - ${Number(row.detected_amount).toFixed(2)} - )} - {row.detected_name && - row.detected_name.toLowerCase() !== bill.name.toLowerCase() && ( - "{row.detected_name}" - )} -
- ))} - {rows.length > 3 && ( - - )} -
-
-
- {importResult ? ( - - ) : ( - - )} - -
-
- ); - })} -
- ); -} - -// ───────────────────────────────────────────────────────────────────────────── - -export function ImportSpreadsheetSection({ onHistoryRefresh }) { - const fileRef = useRef(null); - const [file, setFile] = useState(null); - const [options, setOptions] = useState(INITIAL_OPTIONS); - const [preview, setPreview] = useState({ status: 'idle', data: null, error: null }); - const [decisions, setDecisions] = useState({}); - const [applyState, setApplyState] = useState({ status: 'idle', result: null, error: null }); - const [allBills, setAllBills] = useState([]); - const [categories, setCategories] = useState([]); - const [selectedRows, setSelectedRows] = useState(new Set()); - const [viewMode, setViewMode] = useState('rows'); // 'rows' | 'bills' - const [importingBillId, setImportingBillId] = useState(null); - const [billImportResults, setBillImportResults] = useState(new Map()); // bill_id → { created, updated, errored } - - // Load bills/categories for the decision controls - useEffect(() => { - api.bills().then(setAllBills).catch(() => {}); - api.categories().then(setCategories).catch(() => {}); - }, []); - - const opt = (k, v) => setOptions(prev => ({ ...prev, [k]: v })); - - // ── Preview ────────────────────────────────────────────────────────────────── - const handlePreview = async () => { - if (!file) return; - setPreview({ status: 'loading', data: null, error: null }); - setDecisions({}); - setSelectedRows(new Set()); - setApplyState({ status: 'idle', result: null, error: null }); - setViewMode('rows'); - setImportingBillId(null); - setBillImportResults(new Map()); - try { - const data = await api.previewSpreadsheetImport(file, { - parseAllSheets: options.parseAllSheets, - defaultYear: options.defaultYear ? parseInt(options.defaultYear, 10) : null, - defaultMonth: options.defaultMonth ? parseInt(options.defaultMonth, 10) : null, - }); - setPreview({ status: 'ready', data, error: null }); - setDecisions(buildInitialDecisions(data.rows)); - } catch (err) { - setPreview({ status: 'error', data: null, error: importErrorState(err, 'Preview failed.') }); - } - }; - - // ── Decision update ────────────────────────────────────────────────────────── - const handleDecisionChange = (rowId, decision) => { - setDecisions(prev => ({ ...prev, [rowId]: decision })); - }; - - const handleSelectedChange = (rowId, selected) => { - setSelectedRows(prev => { - const next = new Set(prev); - if (selected) next.add(rowId); - else next.delete(rowId); - return next; - }); - }; - - const clearSelection = () => setSelectedRows(new Set()); - - // ── Bill-history direct import ──────────────────────────────────────────── - // Applies all matching rows for a bill immediately — no queue, no review step. - const handleDirectImportBill = async (group) => { - const sessionId = preview.data?.import_session_id; - if (!sessionId || importingBillId) return; - - const previousResult = billImportResults.get(group.bill.id) ?? null; - const rowsToImport = billImportProgress(group.rows, previousResult).remainingRows; - if (rowsToImport.length === 0) { - toast.info(`All rows for "${group.bill.name}" have already been imported.`); - return; - } - - setImportingBillId(group.bill.id); - try { - const decisionsList = rowsToImport.map(row => ({ - row_id: row.row_id, - action: 'match_existing_bill', - bill_id: group.bill.id, - actual_amount: row.detected_amount ?? null, - payment_amount: row.detected_payment_amount ?? row.detected_amount ?? null, - payment_date: row.detected_paid_date ?? null, - })); - - const result = await api.applySpreadsheetImport({ - import_session_id: sessionId, - decisions: decisionsList, - options: {}, - }); - - const created = result.rows_created ?? 0; - const updated = result.rows_updated ?? 0; - const errored = result.rows_errored ?? 0; - const details = result.details ?? []; - const duplicateRowIds = new Set( - details - .filter(d => d.result === 'skipped_duplicate' || d.payment === 'skipped_duplicate') - .map(d => d.row_id) - .filter(Boolean), - ); - const duplicates = duplicateRowIds.size || (result.rows_duplicates ?? 0); - - // Collect created_at dates from duplicate detail entries so we can show - // when the existing payments were originally recorded. - const dupDates = details - .filter(d => (d.result === 'skipped_duplicate' || d.payment === 'skipped_duplicate') && d.existing_created_at) - .map(d => new Date(d.existing_created_at)) - .filter(d => !isNaN(d.getTime())) - .sort((a, b) => a - b); - - const earliestDup = dupDates[0] ?? null; - const latestDup = dupDates.at(-1) ?? null; - const completedRowIds = new Set(previousResult?.completedRowIds ?? []); - const erroredRowIds = new Set(previousResult?.erroredRowIds ?? []); - - for (const detail of details) { - if (detailCompletesImport(detail)) { - completedRowIds.add(detail.row_id); - erroredRowIds.delete(detail.row_id); - } else if (['error', 'ambiguous', 'skipped_conflict'].includes(detail?.result)) { - erroredRowIds.add(detail.row_id); - } - } - - const mergedResult = { - created: (previousResult?.created ?? 0) + created, - updated: (previousResult?.updated ?? 0) + updated, - errored: erroredRowIds.size, - duplicates: (previousResult?.duplicates ?? 0) + duplicates, - earliestDup: previousResult?.earliestDup && earliestDup - ? (previousResult.earliestDup < earliestDup ? previousResult.earliestDup : earliestDup) - : (previousResult?.earliestDup ?? earliestDup), - latestDup: previousResult?.latestDup && latestDup - ? (previousResult.latestDup > latestDup ? previousResult.latestDup : latestDup) - : (previousResult?.latestDup ?? latestDup), - completedRowIds, - erroredRowIds, - }; - - setBillImportResults(prev => new Map(prev).set(group.bill.id, mergedResult)); - - const imported = created + updated; - const fmtDate = d => d?.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); - const remainingCount = billImportProgress(group.rows, mergedResult).remainingCount; - - if (imported === 0 && duplicates > 0) { - const dateHint = earliestDup - ? ` (first recorded ${fmtDate(earliestDup)})` - : ''; - toast.warning( - remainingCount === 0 - ? `All rows for "${group.bill.name}" are now imported${dateHint}.` - : `${duplicates} row${duplicates > 1 ? 's' : ''} for "${group.bill.name}" already exist${dateHint}. ${remainingCount} remaining.`, - ); - } else { - const parts = [`${imported} entr${imported === 1 ? 'y' : 'ies'} imported`]; - if (duplicates > 0) { - const dateHint = earliestDup ? ` (recorded ${fmtDate(earliestDup)})` : ''; - parts.push(`${duplicates} already existed${dateHint}`); - } - if (errored > 0) parts.push(`${errored} error${errored > 1 ? 's' : ''}`); - if (remainingCount > 0) parts.push(`${remainingCount} remaining`); - toast.success(`${group.bill.name} — ${parts.join(' · ')}`); - } - onHistoryRefresh?.(); - } catch (err) { - toast.error(err.message || `Import failed for "${group.bill.name}"`); - } finally { - setImportingBillId(null); - } - }; - - const selectAllVisibleRows = () => { - setSelectedRows(new Set((preview.data?.rows || []).map(r => r.row_id))); - }; - - const selectedPreviewRows = () => { - const selected = selectedRows; - return (preview.data?.rows || []).filter(r => selected.has(r.row_id)); - }; - - const handleBulkSkip = () => { - const rows = selectedPreviewRows(); - setDecisions(prev => { - const next = { ...prev }; - rows.forEach(row => { - next[row.row_id] = { ...(next[row.row_id] || {}), action: 'skip_row', bill_id: null, bill_name: null }; - }); - return next; - }); - toast.success(`${rows.length} row${rows.length === 1 ? '' : 's'} marked skipped.`); - }; - - const handleBulkCreateNew = () => { - const rows = selectedPreviewRows(); - let missingNames = 0; - setDecisions(prev => { - const next = { ...prev }; - rows.forEach(row => { - const decision = buildCreateNewDecision(row, next[row.row_id] || {}); - if (!decision.bill_name?.trim()) missingNames++; - next[row.row_id] = decision; - }); - return next; - }); - if (missingNames > 0) { - toast.warning(`${missingNames} selected row${missingNames === 1 ? '' : 's'} still need a bill name.`); - } else { - toast.success(`${rows.length} row${rows.length === 1 ? '' : 's'} set to create new bills.`); - } - }; - - const handleBulkReset = () => { - const rows = selectedPreviewRows(); - setDecisions(prev => { - const next = { ...prev }; - rows.forEach(row => { - next[row.row_id] = initialDecisionFromRecommendation(row); - }); - return next; - }); - toast.success(`${rows.length} row${rows.length === 1 ? '' : 's'} reset to recommendation.`); - }; - - const buildApplyDecision = (row, d) => { - if (!d?.action) return null; - - const base = { - row_id: row.row_id, - action: d.action, - actual_amount: d.actual_amount ?? row.detected_amount ?? undefined, - year: row.detected_year ?? undefined, - month: row.detected_month ?? undefined, - notes: d.notes ?? row.detected_notes ?? undefined, - payment_amount: d.payment_amount ?? row.detected_payment_amount ?? undefined, - payment_date: d.payment_date ?? row.detected_paid_date ?? undefined, - }; - - if (d.action === 'create_new_bill') { - return { - ...base, - bill_name: d.bill_name?.trim() || undefined, - category_id: d.category_id ?? undefined, - due_day: d.due_day ?? undefined, - expected_amount: d.expected_amount ?? undefined, - }; - } - - if (ACTIONS_NEEDING_BILL.has(d.action)) { - return { - ...base, - bill_id: d.bill_id ?? undefined, - }; - } - - return base; - }; - - // ── Apply ──────────────────────────────────────────────────────────────────── - const handleApply = async () => { - if (!preview.data) return; - setApplyState({ status: 'loading', result: null, error: null }); - try { - const decisionsList = preview.data.rows - .map(row => { - const d = decisions[row.row_id]; - if (d?.action === 'skip_row') return null; - return buildApplyDecision(row, d); - }) - .filter(Boolean); - - if (decisionsList.length === 0) { - throw new Error('No rows are selected to import. Choose at least one row to match or create, or keep the preview for review.'); - } - - const result = await api.applySpreadsheetImport({ - import_session_id: preview.data.import_session_id, - decisions: decisionsList, - options: { reviewed_skipped_count: skipRows.length }, - }); - setApplyState({ status: 'done', result, error: null }); - setSelectedRows(new Set()); - toast.success(`Import applied — ${result.rows_created} created, ${result.rows_updated} updated.`); - onHistoryRefresh(); - } catch (err) { - const errorState = importErrorState(err, 'Apply failed.'); - setApplyState({ status: 'error', result: null, error: errorState }); - toast.error(errorState.message || 'Apply failed.'); - } - }; - - // ── Reset ──────────────────────────────────────────────────────────────────── - const handleReset = () => { - setFile(null); - setOptions(INITIAL_OPTIONS); - setPreview({ status: 'idle', data: null, error: null }); - setDecisions({}); - setSelectedRows(new Set()); - setApplyState({ status: 'idle', result: null, error: null }); - setViewMode('rows'); - setImportingBillId(null); - setBillImportResults(new Map()); - if (fileRef.current) fileRef.current.value = ''; - }; - - // ── Derived state ──────────────────────────────────────────────────────────── - const previewRows = preview.data?.rows ?? []; - const unresolvedRows = previewRows.filter(r => { - const d = decisions[r.row_id]; - return !d?.action || !isDecisionComplete(d.action, d); - }); - const pendingRows = previewRows.filter(r => decisions[r.row_id]?.action && decisions[r.row_id]?.action !== 'skip_row'); - const skipRows = previewRows.filter(r => decisions[r.row_id]?.action === 'skip_row'); - const canApply = unresolvedRows.length === 0 && pendingRows.length > 0 && applyState.status !== 'loading'; - - // ── Render ──────────────────────────────────────────────────────────────────── - return ( - - - {/* ── Upload panel ──────────────────────────────────────────────────────── */} -
- - {/* File picker */} -
- -
- setFile(e.target.files?.[0] ?? null)} /> - - {file && ( - - {file.name} - - )} -
-
- - {/* Options */} -
-
- opt('parseAllSheets', v)} - id="parse-all" /> - -
-
- - opt('defaultYear', e.target.value)} - className="w-24 h-8 text-sm" /> -
- {!options.parseAllSheets && ( -
- - opt('defaultMonth', e.target.value)} - className="w-20 h-8 text-sm" /> -
- )} -
- - {/* Preview button */} -
- - {(preview.status === 'ready' || preview.status === 'error' || applyState.status !== 'idle') && ( - - )} -
- - {/* Error from preview */} - {preview.status === 'error' && ( -
- {preview.error?.message || preview.error || 'Preview failed.'} - {preview.error?.details?.length > 0 && ( -
    - {preview.error.details.map((d, i) => ( -
  • {d.row_id ? `${d.row_id}: ` : ''}{d.message}
  • - ))} -
- )} -
- )} -
- - {/* ── Preview results ────────────────────────────────────────────────────── */} - {preview.status === 'ready' && preview.data && !['loading', 'done'].includes(applyState.status) && ( -
- - {/* Workbook summary */} - - - {/* Row decision table */} - {previewRows.length > 0 ? ( -
- {/* Tab header */} -
-
- - -
- - {viewMode === 'rows' - ? 'Select rows, apply bulk decisions, then import.' - : 'Click a bill to queue its entire history from this file.'} - -
- - {/* Rows view */} - {viewMode === 'rows' && ( - <> - - - - )} - - {/* Bills view */} - {viewMode === 'bills' && ( - - )} -
- ) : ( -

No data rows found in this file.

- )} - - {/* Apply bar */} - {previewRows.length > 0 && ( -
-
- {previewRows.length} rows reviewed - {pendingRows.length} to apply - {skipRows.length} skipped - {unresolvedRows.length > 0 && ( - {unresolvedRows.length} need a decision - )} -
- -
- )} -
- )} - - {/* ── Applying ──────────────────────────────────────────────────────────── */} - {applyState.status === 'loading' && ( -
- - Applying import… -
- )} - - {/* ── Apply result ──────────────────────────────────────────────────────── */} - {applyState.status === 'done' && applyState.result && ( -
-
-
- -

Import applied successfully

-
-
- {[ - { label: 'Created', value: applyState.result.rows_created, color: 'text-emerald-600' }, - { label: 'Updated', value: applyState.result.rows_updated, color: 'text-blue-600' }, - { label: 'Skipped', value: applyState.result.rows_skipped, color: 'text-muted-foreground' }, - { label: 'Errors', value: applyState.result.rows_errored, color: 'text-red-500' }, - ].map(({ label, value, color }) => ( -
-

{value}

-

{label}

-
- ))} -
-
- -
- )} - - {/* ── Apply error ───────────────────────────────────────────────────────── */} - {applyState.status === 'error' && ( -
-
- {applyState.error?.message || applyState.error || 'Apply failed.'} - {applyState.error?.details?.length > 0 && ( -
    - {applyState.error.details.map((d, i) => ( -
  • - {d.row_id ? `${d.row_id}: ` : ''} - {d.field ? `${d.field} - ` : ''} - {d.message} -
  • - ))} -
- )} - {applyState.error?.error_id && ( -

Error ID: {applyState.error.error_id}

- )} -
-
- )} -
- ); -} - -// ─── DataPage ───────────────────────────────────────────────────────────────── - -function SeedDemoDataSection({ onSeeded }) { - const [loading, setLoading] = useState(false); - const [seeded, setSeeded] = useState(false); - const [counts, setCounts] = useState({ bills: 0, categories: 0 }); - const [clearing, setClearing] = useState(false); - const [showClearConfirm, setShowClearConfirm] = useState(false); - const [statusLoading, setStatusLoading] = useState(true); - - useEffect(() => { - api.seededStatus() - .then(data => { - setSeeded(data.seeded); - if (data.seeded) setCounts({ bills: data.seededBills || 0, categories: data.seededCategories || 0 }); - }) - .catch(err => console.error('Failed to check seeded status:', err)) - .finally(() => setStatusLoading(false)); - }, []); - - const handleSeed = async () => { - setLoading(true); - try { - const data = await api.seedDemoData(); - if (!data || typeof data !== 'object') throw new Error('Invalid response from server'); - setCounts({ bills: data.billsCreated || 0, categories: data.categoriesCreated || 0 }); - setSeeded(true); - toast.success(`Created ${data.billsCreated || 0} demo bills successfully.`); - setTimeout(() => onSeeded?.(), 100); - } catch (err) { - console.error('Seed error:', err); - toast.error(err?.message || err?.error || 'Failed to seed demo data.'); - } finally { - setLoading(false); - } - }; - - const handleClearDemoData = async () => { - setClearing(true); - try { - const data = await api.clearDemoData(); - setSeeded(false); - setCounts({ bills: 0, categories: 0 }); - setShowClearConfirm(false); - toast.success(`Removed ${data.billsDeleted || 0} demo bills and ${data.categoriesDeleted || 0} demo categories.`); - onSeeded?.(); - } catch (err) { - toast.error(err.message || 'Failed to clear demo data.'); - } finally { - setClearing(false); - } - }; - - return ( - -
- {statusLoading ? ( -

Loading…

- ) : seeded ? ( - <> -

Demo data seeded

-
-
-

Bills

-

{counts.bills}

-
-
-

Categories

-

{counts.categories}

-
-
- - ) : ( -

- Create 20 realistic demo bills and 8 demo categories for testing purposes. - The data will be associated with your account. -

- )} - -
- - - - - - - - - Clear Demo Data - - This will remove {counts.bills} demo bills and {counts.categories} demo categories from your account. This cannot be undone. - - - - Cancel - - {clearing ? <>Clearing… : 'Clear Data'} - - - - -
-
-
- ); -} +import ImportTransactionCsvSection from '@/components/data/ImportTransactionCsvSection'; +import TransactionMatchingSection from '@/components/data/TransactionMatchingSection'; +import ImportSpreadsheetSection from '@/components/data/ImportSpreadsheetSection'; +import ImportMyDataSection from '@/components/data/ImportMyDataSection'; +import SeedDemoDataSection from '@/components/data/SeedDemoDataSection'; +import DownloadMyDataSection from '@/components/data/DownloadMyDataSection'; +import ImportHistorySection from '@/components/data/ImportHistorySection'; export default function DataPage() { const [history, setHistory] = useState(null); diff --git a/client/pages/TrackerPage.jsx b/client/pages/TrackerPage.jsx index 35a1b11..e75ad1a 100644 --- a/client/pages/TrackerPage.jsx +++ b/client/pages/TrackerPage.jsx @@ -25,6 +25,9 @@ import { } from '@/components/ui/select'; import { Label } from '@/components/ui/label'; +import MonthlyStateDialog from '@/components/tracker/MonthlyStateDialog'; +import StartingAmountsEditDialog from '@/components/tracker/StartingAmountsEditDialog'; +import PaymentModal from '@/components/tracker/PaymentModal'; const MONTHS = [ 'January','February','March','April','May','June', 'July','August','September','October','November','December', @@ -780,443 +783,6 @@ function NotesCell({ row, refresh }) { ); } -// ── Monthly state dialog ─────────────────────────────────────────────────── -// Edits actual_amount, monthly notes, and is_skipped for a specific bill+month. -// Changes are isolated to the selected month — other months are not affected. -function MonthlyStateDialog({ row, year, month, open, onOpenChange, onSaved }) { - const [actualAmount, setActualAmount] = useState(''); - const [notes, setNotes] = useState(''); - const [isSkipped, setIsSkipped] = useState(false); - const [saving, setSaving] = useState(false); - - // Populate from current row state when dialog opens - useEffect(() => { - if (open) { - setActualAmount(row.actual_amount != null ? String(row.actual_amount) : ''); - setNotes(row.monthly_notes || ''); - setIsSkipped(!!row.is_skipped); - } - }, [open, row]); - - async function handleSave(e) { - e.preventDefault(); - const amt = actualAmount.trim() ? parseFloat(actualAmount) : null; - if (amt !== null && (isNaN(amt) || amt < 0)) { - toast.error('Amount must be a positive number or empty'); - return; - } - setSaving(true); - try { - await api.saveBillMonthlyState(row.id, { - year, - month, - actual_amount: amt, - notes: notes.trim() || null, - is_skipped: isSkipped, - }); - toast.success(`${MONTHS[month - 1]} state saved`); - onSaved(); - onOpenChange(false); - } catch (err) { - toast.error(err.message); - } finally { - setSaving(false); - } - } - - return ( - - - - - {row.name} - - {MONTHS[month - 1]} {year} - - -

- Monthly overrides — changes only affect {MONTHS[month - 1]} -

-
- -
- {/* Actual amount this month */} -
- - setActualAmount(e.target.value)} - className="font-mono bg-background/50 border-border/60" - /> -

- Leave blank to use the template default ({fmt(row.expected_amount)}). -

-
- - {/* Monthly notes */} -
- - setNotes(e.target.value)} - placeholder="e.g. higher than usual, double-billed…" - className="bg-background/50 border-border/60" - /> -
- - {/* Skip this month */} - -
- - - - - -
-
- ); -} - -function StartingAmountsEditDialog({ open, onClose, year, month, onSave }) { - const [loading, setLoading] = useState(false); - const [saving, setSaving] = useState(false); - const [error, setError] = useState(''); - const [firstAmount, setFirstAmount] = useState('0'); - const [fifteenthAmount, setFifteenthAmount] = useState('0'); - const [otherAmount, setOtherAmount] = useState('0'); - const [preview, setPreview] = useState(null); - - const monthName = `${MONTHS[month - 1]} ${year}`; - const localFirst = Number(firstAmount) || 0; - const localFifteenth = Number(fifteenthAmount) || 0; - const localOther = Number(otherAmount) || 0; - const totalStarting = localFirst + localFifteenth + localOther; - const paidSoFar = Number(preview?.paid_total || 0); - const firstRemaining = localFirst - Number(preview?.paid_from_first || 0); - const fifteenthRemaining = localFifteenth - Number(preview?.paid_from_fifteenth || 0); - const totalRemaining = totalStarting - paidSoFar; - - useEffect(() => { - let alive = true; - async function loadStartingAmounts() { - if (!open) return; - setLoading(true); - setError(''); - try { - const result = await api.getMonthlyStartingAmounts(year, month); - if (!alive) return; - setPreview(result); - setFirstAmount(String(result.first_amount ?? 0)); - setFifteenthAmount(String(result.fifteenth_amount ?? 0)); - setOtherAmount(String(result.other_amount ?? 0)); - } catch (err) { - if (!alive) return; - setError(err.message || 'Monthly starting amounts could not be loaded.'); - } finally { - if (alive) setLoading(false); - } - } - loadStartingAmounts(); - return () => { alive = false; }; - }, [open, year, month]); - - async function handleSave(e) { - e.preventDefault(); - const first = Number(firstAmount); - const fifteenth = Number(fifteenthAmount); - const other = Number(otherAmount); - if (![first, fifteenth, other].every(value => Number.isFinite(value) && value >= 0)) { - setError('Starting amounts must be non-negative numbers.'); - return; - } - - setSaving(true); - setError(''); - try { - await api.updateMonthlyStartingAmounts({ - year, - month, - first_amount: first, - fifteenth_amount: fifteenth, - other_amount: other, - }); - toast.success('Monthly starting amounts saved.'); - onSave(); - } catch (err) { - setError(err.message || 'Monthly starting amounts could not be saved.'); - } finally { - setSaving(false); - } - } - - return ( - { if (!value) onClose(); }}> - - - Monthly Starting Amounts -

{monthName}

-
- -
- {error && ( -
- {error} -
- )} - -
- - - -
- -
-
-
-

Total starting

-

{fmt(totalStarting)}

-
-
-

Paid so far

-

{fmt(paidSoFar)}

-
-
-

Total remaining

-

{fmt(totalRemaining)}

-
-
-
-
- 1st remaining - {fmt(firstRemaining)} -
-
- 15th remaining - {fmt(fifteenthRemaining)} -
-
- Other - {fmt(localOther)} -
-
-
-
- - - - - -
-
- ); -} - -// ── Payment modal ────────────────────────────────────────────────────────── -function PaymentModal({ payment, onClose, onSave }) { - const [amount, setAmount] = useState(String(payment.amount)); - const [date, setDate] = useState(payment.paid_date); - // Use METHOD_NONE sentinel — empty string value crashes Radix Select - const [method, setMethod] = useState(payment.method || METHOD_NONE); - const [notes, setNotes] = useState(payment.notes || ''); - const [busy, setBusy] = useState(false); - const [confirmDelete, setConfirmDelete] = useState(false); - - async function handleSave(e) { - e.preventDefault(); - setBusy(true); - try { - await api.updatePayment(payment.id, { - amount: parseFloat(amount), - paid_date: date, - method: method === METHOD_NONE ? null : method, - notes: notes || null, - }); - toast.success('Payment saved'); - onSave(); onClose(); - } catch (err) { - toast.error(err.message); - } finally { setBusy(false); } - } - - async function handleDelete() { - setBusy(true); - try { - await api.deletePayment(payment.id); - toast.success('Payment moved to recovery. Bill is now marked as unpaid.', { - action: { - label: 'Undo', - onClick: async () => { - try { - await api.restorePayment(payment.id); - toast.success('Payment restored'); - onSave(); - } catch (err) { - toast.error(err.message || 'Failed to restore payment'); - } - }, - }, - }); - onSave(); onClose(); - } catch (err) { - toast.error(err.message); - } finally { setBusy(false); } - } - - return ( - <> - { if (!v) onClose(); }}> - - - Edit Payment - - -
-
- - setAmount(e.target.value)} - className="font-mono bg-background/50 border-border/60" /> -
-
- - setDate(e.target.value)} - className="font-mono bg-background/50 border-border/60" /> -
-
- - -
-
- - setNotes(e.target.value)} - className="bg-background/50 border-border/60" /> -
-
- - - -
- - -
-
-
-
- - - - - Remove this payment? - - This marks the payment as removed and reverses any debt balance update. You can undo it from the toast. - - - - Cancel - - {busy ? 'Removing...' : 'Remove Payment'} - - - - - - ); -} - // ── Table row ────────────────────────────────────────────────────────────── function Row({ row, year, month, refresh, index, onEditBill }) { const amountRef = useRef(null); diff --git a/client/public/img/apple-touch-icon.png b/client/public/img/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..06c79f770da6194f82fa7d25391f9b63fd38afb8 GIT binary patch literal 14921 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 z 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 + + + + + + Bill Tracker diff --git a/package-lock.json b/package-lock.json index ac4b83d..f8ffcd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bill-tracker", - "version": "0.28.3", + "version": "0.28.4.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bill-tracker", - "version": "0.28.3", + "version": "0.28.4.4", "license": "ISC", "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.2", @@ -52,7 +52,8 @@ "concurrently": "^9.1.0", "postcss": "^8.4.47", "tailwindcss": "^3.4.14", - "vite": "^5.4.10" + "vite": "^5.4.10", + "vite-plugin-pwa": "^1.3.0" } }, "node_modules/@alloc/quick-lru": { @@ -67,14 +68,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "node_modules/@apideck/better-ajv-errors": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.7.tgz", + "integrity": "sha512-TajUJwGWbDwkCx/CZi7tRE8PVB7simCvKJfHUsSdvps+aTM/PDPP4gkLmKnc+x3CE//y9i/nj74GqdL/hwk7Iw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "jsonpointer": "^5.0.1", + "leven": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -83,9 +101,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", - "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -124,14 +142,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -140,15 +158,28 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "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" @@ -157,40 +188,20 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@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" }, "engines": { "node": ">=6.9.0" @@ -199,20 +210,174 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "dev": true, + "license": "MIT", + "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" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -220,9 +385,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -230,15 +395,30 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helpers": { "version": "7.29.2", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", @@ -254,13 +434,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", - "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -269,6 +449,828 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", + "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", + "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", + "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", + "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", + "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", + "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", + "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", + "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", + "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", + "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", + "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", + "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", + "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", + "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", + "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", + "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz", + "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==", + "dev": true, + "license": "MIT", + "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.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", + "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", + "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", + "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", + "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", @@ -301,34 +1303,342 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", + "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", + "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", + "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz", + "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", + "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", + "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", + "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", + "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz", + "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -336,14 +1646,14 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -778,6 +2088,16 @@ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -808,6 +2128,17 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -1930,6 +3261,139 @@ "dev": true, "license": "MIT" }, + "node_modules/@rollup/plugin-babel": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.1.0.tgz", + "integrity": "sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@rollup/pluginutils": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.3.tgz", + "integrity": "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz", + "integrity": "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^7.0.3", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.3", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", @@ -2372,6 +3836,22 @@ "react": "^18 || ^19" } }, + "node_modules/@trickfilm400/rollup-plugin-off-main-thread": { + "version": "3.0.0-pre1", + "resolved": "https://registry.npmjs.org/@trickfilm400/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-3.0.0-pre1.tgz", + "integrity": "sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "ejs": "^3.1.10", + "json5": "^2.2.3", + "magic-string": "^0.30.21", + "string.prototype.matchall": "^4.0.12" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2475,6 +3955,20 @@ "csstype": "^3.2.2" } }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -2521,6 +4015,19 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/adler-32": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", @@ -2530,6 +4037,23 @@ "node": ">=0.8" } }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2593,12 +4117,78 @@ "node": ">=10" } }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/autoprefixer": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", @@ -2636,6 +4226,64 @@ "postcss": "^8.1.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -2646,6 +4294,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2770,6 +4428,19 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -2840,6 +4511,13 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -2849,6 +4527,25 @@ "node": ">= 0.8" } }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -3127,6 +4824,16 @@ "node": ">= 6" } }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/concurrently": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", @@ -3208,6 +4915,20 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -3237,6 +4958,31 @@ "node": ">=0.8" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -3256,6 +5002,60 @@ "license": "MIT", "peer": true }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -3310,6 +5110,52 @@ "node": ">=4.0.0" } }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3398,6 +5244,22 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.353", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz", @@ -3430,6 +5292,75 @@ "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", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3460,6 +5391,40 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -3537,6 +5502,36 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eta": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-4.6.0.tgz", + "integrity": "sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/bgub/eta?sponsor=1" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -3640,6 +5635,13 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -3668,6 +5670,30 @@ "node": ">= 6" } }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -3683,6 +5709,46 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "license": "MIT" }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3728,6 +5794,39 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3775,6 +5874,22 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3798,6 +5913,47 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3851,6 +6007,13 @@ "node": ">=6" } }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true, + "license": "ISC" + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -3864,12 +6027,55 @@ "node": ">= 0.4" } }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "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": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3882,6 +6088,23 @@ "node": ">=10.13.0" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3894,6 +6117,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3904,6 +6147,35 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -3916,6 +6188,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", @@ -4025,6 +6313,13 @@ "node": ">=0.10.0" } }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "dev": true, + "license": "ISC" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -4063,6 +6358,21 @@ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", "license": "MIT" }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ip-address": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", @@ -4105,6 +6415,60 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -4117,6 +6481,36 @@ "node": ">=8" } }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.2", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", @@ -4132,6 +6526,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-decimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", @@ -4151,6 +6580,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -4161,6 +6606,26 @@ "node": ">=8" } }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4183,6 +6648,39 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4192,6 +6690,33 @@ "node": ">=0.12.0" } }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -4204,6 +6729,222 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -4241,6 +6982,13 @@ "node": ">=6" } }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4254,6 +7002,39 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -4272,6 +7053,20 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true, + "license": "MIT" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -4313,6 +7108,16 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -5259,6 +8064,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -5268,6 +8089,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -5412,6 +8243,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/oidc-token-hash": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz", @@ -5475,6 +8337,31 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parse-entities": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", @@ -5509,12 +8396,49 @@ "node": ">= 0.8" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/path-to-regexp": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", @@ -5557,6 +8481,16 @@ "node": ">= 6" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.5.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", @@ -5740,6 +8674,19 @@ "node": ">=10" } }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -5773,6 +8720,16 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.15.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", @@ -6045,6 +9002,108 @@ "node": ">=8.10.0" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz", + "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, "node_modules/rehype-sanitize": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz", @@ -6135,6 +9194,16 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -6244,6 +9313,26 @@ "tslib": "^2.1.0" } }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -6264,6 +9353,41 @@ ], "license": "MIT" }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -6328,6 +9452,16 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/serialize-javascript": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", + "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/serve-static": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", @@ -6343,12 +9477,84 @@ "node": ">= 0.8.0" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/shell-quote": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", @@ -6434,6 +9640,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -6479,6 +9698,16 @@ "simple-concat": "^1.0.0" } }, + "node_modules/smob": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.2.tgz", + "integrity": "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/sonner": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz", @@ -6489,6 +9718,20 @@ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6498,6 +9741,27 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/space-separated-tokens": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", @@ -6529,6 +9793,20 @@ "node": ">= 0.8" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -6553,6 +9831,93 @@ "node": ">=8" } }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -6567,6 +9932,21 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -6580,6 +9960,16 @@ "node": ">=8" } }, + "node_modules/strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -6750,6 +10140,61 @@ "node": ">=6" } }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -6837,6 +10282,16 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -6891,6 +10346,19 @@ "node": "*" } }, + "node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -6904,6 +10372,147 @@ "node": ">= 0.6" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -6923,6 +10532,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/unist-util-is": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", @@ -6991,6 +10613,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -7000,6 +10632,17 @@ "node": ">= 0.8" } }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "yarn": "*" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -7186,6 +10829,161 @@ } } }, + "node_modules/vite-plugin-pwa": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.3.0.tgz", + "integrity": "sha512-c5kMgN+ITrOtHXp8PAtk2uOIEea6XjP/unCGxOWWBzQ6qa65qj/awHg0wf+QF9E/2u9vh86LqxPwzEPNbM2r5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.6", + "pretty-bytes": "^6.1.1", + "tinyglobby": "^0.2.10", + "workbox-build": "^7.4.1", + "workbox-window": "^7.4.1" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vite-pwa/assets-generator": "^1.0.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "workbox-build": "^7.4.1", + "workbox-window": "^7.4.1" + }, + "peerDependenciesMeta": { + "@vite-pwa/assets-generator": { + "optional": true + } + } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.21.tgz", + "integrity": "sha512-zbRA8cVm6io/d5W8uIe2hblzN76/Wm3v/yiythQvr+dpBWeqhPSWIDNj4zOyHi4zKbMK6DN34Xsr9jPHJERAEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wmf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", @@ -7204,6 +11002,226 @@ "node": ">=0.8" } }, + "node_modules/workbox-background-sync": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.1.tgz", + "integrity": "sha512-HhT7KE8tOWDm02wRNshXUnUPofMlhenF2DBdUnDPOubhizzPeItkYTmAB6td1Z2cjYPa98vzEiPLEuzn5hN66g==", + "dev": true, + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-broadcast-update": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.1.tgz", + "integrity": "sha512-uAlgslKLvbQY+suirIdnBCSYrcgBhjp81Nj4l1lj/Jmj0MJO2CJERnCJjT0GFVwmReV0N+zs78K6gqd5gr9/+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-build": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.1.tgz", + "integrity": "sha512-SDhxIvEAde9Gy/5w4Yo1Jh/M49Z0qE3q0oteyE8zGq0DScxFqVBcCtIXFuLtmtxRQZCMbf0prco4VyEu3KBQuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.24.4", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.3", + "@rollup/plugin-replace": "^6.0.3", + "@rollup/plugin-terser": "^1.0.0", + "@trickfilm400/rollup-plugin-off-main-thread": "^3.0.0-pre1", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "eta": "^4.5.1", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^11.0.1", + "pretty-bytes": "^5.3.0", + "rollup": "^4.53.3", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "7.4.1", + "workbox-broadcast-update": "7.4.1", + "workbox-cacheable-response": "7.4.1", + "workbox-core": "7.4.1", + "workbox-expiration": "7.4.1", + "workbox-google-analytics": "7.4.1", + "workbox-navigation-preload": "7.4.1", + "workbox-precaching": "7.4.1", + "workbox-range-requests": "7.4.1", + "workbox-recipes": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1", + "workbox-streams": "7.4.1", + "workbox-sw": "7.4.1", + "workbox-window": "7.4.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/workbox-build/node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/workbox-cacheable-response": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.1.tgz", + "integrity": "sha512-8xaFoJdDc2OjrlbbL3gEeBO1WKcMwRqwLRupgqahYXu75yXajPLuwrbXMrIGZuWYXrQwk0xDjOxZ/ujCy/oJYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-core": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.1.tgz", + "integrity": "sha512-DT+vu46eh/2vRsSHTY4Xmc32Z1rr9PRlQUXr1Dx30ZuXRWwOsvZgGgcwxcasubQLQmbTNYZjv44LkBAQ4tT5tQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-expiration": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.1.tgz", + "integrity": "sha512-lRKUF7b+OGbeXkQk1s6MHXOa3d7Xxf7Of31W6c6hCfipfIyrtdWZ89stq21AHZMaoG7VNFoHply4Ox+rU31TWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-google-analytics": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.1.tgz", + "integrity": "sha512-Mks1JwLEt++ZAkF6sS1OpSh9RtAMIsiDgRpK+codiHGIPXeaUOgi4cPc3GFadUl8V5QPeypEk8Oxgl3HlwVzHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-background-sync": "7.4.1", + "workbox-core": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" + } + }, + "node_modules/workbox-navigation-preload": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.1.tgz", + "integrity": "sha512-C4KVsjPcYKJOhr631AxR9XoG2rLF3QiTk5aMv36MXOjtWvm8axwNFAtKUPGsWUwLXXAMgYM1En7fsvndaXeXRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-precaching": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.1.tgz", + "integrity": "sha512-cdr/9qByww7yzEp7zg/qI4ukUrrNjQLgN+ONQRpjy/VqGQXwkgHwr00KksGJK8v0VifwDXBb8a4cWNZH71jn3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" + } + }, + "node_modules/workbox-range-requests": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.1.tgz", + "integrity": "sha512-7i2oxAUE82gHdAJBCAQ04JzNOdRPqzuOzGfoUyJpFSmeqBNYGPrAH8GPoPjUQTfp+NycwrD2H68VtuF8qxv0vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-recipes": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.1.tgz", + "integrity": "sha512-gnbVfmV4/TtmQaM4x9AtuXhcdstJsep3XMVeztOrQVPT+R6+6DeBjGTCQ7fFCXm+4GEHUA5VEBTyi5+4gWGeog==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-cacheable-response": "7.4.1", + "workbox-core": "7.4.1", + "workbox-expiration": "7.4.1", + "workbox-precaching": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" + } + }, + "node_modules/workbox-routing": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.1.tgz", + "integrity": "sha512-yubJGErZOusuidAenaL5ypfhQOa7urxP/f8E0ws7FPb4039RiWXUWBAyUkmUoOL/BcQGen3h0J8872d51IYxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-strategies": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.1.tgz", + "integrity": "sha512-GZxpaw9NbmOelj7667uZ2kpk5BFpOGbO4X0qjwh5ls8XQ8C+Lha5LQchTiUzsTFSS+NlUpftYAyOVXvQUrcqOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-streams": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.1.tgz", + "integrity": "sha512-HWWtraKUbJknd9kgqGcpQ3G114HOPYvqs8HaJMDs2ebLNAimDkVDaWfAXE6Ybl+m8U6KsCE6pWyLYuigWmnAXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1", + "workbox-routing": "7.4.1" + } + }, + "node_modules/workbox-sw": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.1.tgz", + "integrity": "sha512-fez5f2DUlDJWTFYkCWQpY10N8gtztd849NswCbVFk0QlcSM4HT5A8x4g4ii650yem4I8tHY0R7JZahwp3ltIPw==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-window": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.1.tgz", + "integrity": "sha512-notZDH2u8VXaqyuD7xaqIfEFi6SRM4SUSd7ewe9PDsVqADuepxX2ZMY3uvuZGxzY5ZOsGC/vD3A/3smFtJt4/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "7.4.1" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 3568b18..826bc90 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bill-tracker", - "version": "0.28.4.4", + "version": "0.29.0", "description": "Monthly bill tracking system", "main": "server.js", "scripts": { @@ -57,7 +57,8 @@ "concurrently": "^9.1.0", "postcss": "^8.4.47", "tailwindcss": "^3.4.14", - "vite": "^5.4.10" + "vite": "^5.4.10", + "vite-plugin-pwa": "^1.3.0" }, "directories": { "doc": "docs" diff --git a/vite.config.mjs b/vite.config.mjs index 70ecd0e..df354b2 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -1,5 +1,6 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; +import { VitePWA } from 'vite-plugin-pwa'; import path from 'path'; import { fileURLToPath } from 'url'; import { createRequire } from 'module'; @@ -9,7 +10,40 @@ const require = createRequire(import.meta.url); const pkg = require('./package.json'); export default defineConfig({ - plugins: [react()], + plugins: [ + react(), + VitePWA({ + registerType: 'autoUpdate', + includeAssets: ['img/logo.png', 'img/pwa-192.png', 'img/pwa-512.png'], + manifest: { + name: 'BillTracker', + short_name: 'BillTracker', + description: 'Personal bill planning and tracking', + theme_color: '#18181b', + background_color: '#18181b', + display: 'standalone', + start_url: '/', + icons: [ + { src: '/img/pwa-192.png', sizes: '192x192', type: 'image/png' }, + { src: '/img/pwa-512.png', sizes: '512x512', type: 'image/png', purpose: 'any maskable' }, + ], + }, + workbox: { + globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'], + runtimeCaching: [ + { + urlPattern: /^\/api\/(tracker|bills|calendar|summary|analytics|snowball|categories)/, + handler: 'NetworkFirst', + options: { + cacheName: 'api-cache', + networkTimeoutSeconds: 5, + expiration: { maxEntries: 30, maxAgeSeconds: 300 }, + }, + }, + ], + }, + }), + ], publicDir: 'client/public', define: { // Injected at build time — frontend reads this instead of maintaining a