diff --git a/CHARTER.md b/CHARTER.md index fe852b8..63fa7db 100644 --- a/CHARTER.md +++ b/CHARTER.md @@ -100,9 +100,34 @@ Provide a unified life management and action dashboard that ingests data from 15 | Technical Lead | @chittyos-infrastructure | | Contact | chittycommand@chitty.cc | +## Three Aspects (TY VY RY) + +Source: `chittycanon://gov/governance#three-aspects` + +| Aspect | Abbrev | Question | ChittyCommand Answer | +|--------|--------|----------|--------------------| +| **Identity** | TY | What IS it? | Unified life management dashboard — ingests financial, legal, and administrative data from 15+ sources, scores urgency, recommends and executes actions | +| **Connectivity** | VY | How does it ACT? | Cron-scheduled syncs (Plaid, Mercury, court dockets, utilities); bridge API to ChittyScrape, ChittyLedger, ChittyFinance; MCP server for Claude-driven queries; action execution via API, email, or browser automation | +| **Authority** | RY | Where does it SIT? | Tier 5 Application — consumer of upstream data, not source of truth; delegates scraping to ChittyScrape, identity to ChittyID, financials to ChittyFinance | + +## Document Triad + +This charter is part of a synchronized documentation triad. Changes to shared fields must propagate. + +| Field | Canonical Source | Also In | +|-------|-----------------|---------| +| Canonical URI | CHARTER.md (Classification) | CHITTY.md (blockquote) | +| Tier | CHARTER.md (Classification) | CHITTY.md (blockquote) | +| Domain | CHARTER.md (Classification) | CHITTY.md (blockquote), CLAUDE.md (header) | +| Endpoints | CHARTER.md (API Contract) | CHITTY.md (Endpoints table), CLAUDE.md (API section) | +| Dependencies | CHARTER.md (Dependencies) | CHITTY.md (Dependencies table), CLAUDE.md (Architecture) | +| Certification badge | CHITTY.md (Certification) | CHARTER.md frontmatter `status` | + +**Related docs**: [CHITTY.md](CHITTY.md) (badge/one-pager) | [CLAUDE.md](CLAUDE.md) (developer guide) + ## Compliance -- [ ] Service registered in ChittyRegistry +- [x] Service registered in ChittyRegister (03-1-USA-3846-T-2602-0-57, pending_cert) - [x] Health endpoint operational at /health - [x] Status endpoint operational at /api/v1/status - [x] CLAUDE.md development guide present @@ -110,4 +135,4 @@ Provide a unified life management and action dashboard that ingests data from 15 - [x] CHITTY.md present --- -*Charter Version: 1.0.0 | Last Updated: 2026-02-24* +*Charter Version: 1.0.0 | Last Updated: 2026-02-23* diff --git a/CLAUDE.md b/CLAUDE.md index 98599af..821d041 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,68 +5,86 @@ ChittyCommand is a unified life management and action dashboard for the ChittyOS ecosystem. It ingests data from 15+ financial, legal, and administrative sources, scores urgency with AI, recommends actions, and executes them via APIs, email, or browser automation. **Repo:** `CHITTYOS/chittycommand` -**Deploy:** Cloudflare Workers + Pages at `command.chitty.cc` -**Stack:** Hono TypeScript, React + Shadcn UI, Neon PostgreSQL, Cloudflare R2 +**Deploy:** Cloudflare Workers at `command.chitty.cc` +**Stack:** Hono TypeScript, React + Tailwind, Neon PostgreSQL (via Hyperdrive), Cloudflare R2/KV +**Canonical URI:** `chittycanon://core/services/chittycommand` | Tier 5 ## Common Commands ```bash npm run dev # Start Hono dev server (wrangler dev) npm run deploy # Deploy to Cloudflare Workers -npm run ui:dev # Start React frontend dev server +npm run ui:dev # Start React frontend dev server (localhost:5173) npm run ui:build # Build frontend for Pages npm run db:generate # Generate Drizzle migrations npm run db:migrate # Run Drizzle migrations ``` -## Architecture +Secrets are managed via wrangler (never hardcode): +```bash +wrangler secret put PLAID_CLIENT_ID +wrangler secret put PLAID_SECRET +wrangler secret put DATABASE_URL +``` -### Workers +## Architecture -| Worker | Domain | Role | -|--------|--------|------| -| command-api | command.chitty.cc | Core API, CRUD, action execution | -| command-ingest | Cron-triggered | Data ingestion from all sources | -| command-ai | Internal | AI triage, urgency scoring, recommendations | -| command-ui | app.command.chitty.cc | React SPA (Cloudflare Pages) | +Single Cloudflare Worker (`chittycommand`) serving API + cron. Frontend is a separate React SPA at `app.command.chitty.cc` (Cloudflare Pages). ### Data Sources -**API Sources (auto-sync):** Mercury, Wave, Stripe, TurboTenant, ChittyRental/ChittyFinance +**Via ChittyFinance (auto-sync):** Mercury, Wave Accounting, Stripe, Plaid +**Direct API:** ChittyBooks, ChittyAssets, ChittyCharge, ChittyLedger +**Via ChittyScrape (bridge):** Mr. Cooper mortgage, Cook County property tax, Court docket **Email Parse:** ComEd, Peoples Gas, Xfinity, Citi, Home Depot, Lowe's -**Scraper:** Mr. Cooper, Cook County property tax, Court docket **Manual:** IRS quarterly, HOA fees, Personal loans **Historical Only:** DoorLoop (sunset, data archived) +### Auth Flow + +Three auth layers in `src/middleware/auth.ts`: +1. **`authMiddleware`** (`/api/*`) — KV token lookup, then ChittyAuth fallback +2. **`bridgeAuthMiddleware`** (`/api/bridge/*`) — Service token OR user token +3. **`mcpAuthMiddleware`** (`/mcp/*`) — Shared service token from KV (bypassed in dev) + +### Cron Schedule + +Defined in `wrangler.toml`, dispatched via `src/lib/cron.ts`: +- `0 12 * * *` — Daily 6 AM CT: Plaid + ChittyFinance sync +- `0 13 * * *` — Daily 7 AM CT: Court docket check +- `0 14 * * 1` — Weekly Mon 8 AM CT: Utility scrapers +- `0 15 1 * *` — Monthly 1st 9 AM CT: Mortgage, property tax + ### Database -Neon PostgreSQL with `cc_` prefixed tables. Schema in `src/db/schema.ts`, SQL migrations in `migrations/`. +Neon PostgreSQL via Hyperdrive binding. All tables prefixed `cc_`. Schema in `src/db/schema.ts`, SQL migrations in `migrations/` (0001-0007). ### Action Execution Three modes: -1. **API** — Mercury transfers, Stripe payments, TurboTenant/ChittyRental +1. **API** — Mercury transfers, Stripe payments via bridge routes 2. **Claude in Chrome** — Browser automation for portals without APIs 3. **Email** — Dispute letters, follow-ups via Cloudflare Email Workers -## Active Disputes - -1. **Xfinity** — Pricing/credit dispute (priority 2) -2. **Commodore Green Briar Landmark Condo Association** — HOA dispute (priority 3) -3. **Fox Rental** — $14K+ reclaim (priority 1) - ## Key Files -- `src/index.ts` — Hono API entry point -- `src/db/schema.ts` — Drizzle schema for all tables +- `src/index.ts` — Hono entry point, route mounting, health/status endpoints +- `src/middleware/auth.ts` — Auth middleware (user, bridge, MCP) +- `src/lib/cron.ts` — Cron sync orchestrator (all data sources) +- `src/lib/integrations.ts` — Service clients (Mercury, Plaid, ChittyScrape, etc.) - `src/lib/urgency.ts` — Deterministic urgency scoring engine -- `src/routes/` — API route handlers -- `migrations/` — SQL migration files -- `ui/` — React frontend +- `src/lib/validators.ts` — Zod schemas for request validation +- `src/routes/bridge.ts` — Inter-service bridge (scrape, ledger, finance, Plaid) +- `src/routes/mcp.ts` — MCP server for Claude integration +- `src/routes/dashboard.ts` — Dashboard summary with urgency scoring +- `src/db/schema.ts` — Drizzle schema for all cc_* tables +- `migrations/` — SQL migration files (0001-0007) +- `ui/` — React frontend (Vite + Tailwind) ## Security -- Credentials via 1Password (`op run`) -- No hardcoded secrets +- Credentials via 1Password (`op run`) — never expose in terminal output +- Secrets via `wrangler secret put` — never in `[vars]` - R2 for document storage (zero egress) -- CORS restricted to `app.command.chitty.cc` and localhost +- CORS restricted to `app.command.chitty.cc`, `command.mychitty.com`, `chittycommand-ui.pages.dev`, `localhost:5173` +- Service tokens stored in KV: `bridge:service_token`, `mcp:service_token`, `scrape:service_token` diff --git a/docs/plans/2026-02-23-ui-overhaul-design.md b/docs/plans/2026-02-23-ui-overhaul-design.md new file mode 100644 index 0000000..d966b86 --- /dev/null +++ b/docs/plans/2026-02-23-ui-overhaul-design.md @@ -0,0 +1,118 @@ +# ChittyCommand UI Overhaul — "Command Console" + +**Date:** 2026-02-23 +**Status:** Approved +**Canonical URI:** `chittycanon://core/services/chittycommand#ui-overhaul` + +## Design Direction + +Hybrid of Bloomberg Terminal (information density, real-time data flow) and Apple Finance (clean cards, quality typography, refined aesthetics). Designed ADHD/neurospicy-first. + +## Visual Language + +### Color Scheme: Dark Chrome + Light Cards +- **Shell** (sidebar, nav, status bar): Dark background (`#1a1a2e` → `#16213e` range) +- **Card surfaces**: White/near-white (`#ffffff` / `#fafafa`) +- **Urgency accents**: Red (`#ef4444`), Amber (`#f59e0b`), Green (`#22c55e`) +- **Text**: Dark on light cards, light on dark chrome +- **Muted state**: Low-urgency cards use lighter text (`#9ca3af`), no border accent + +### Typography +- **Display/headings**: Outfit (geometric sans-serif) — clean, modern, highly legible +- **Body/data**: Outfit at smaller weights — consistent family, no font-switching fatigue +- **Monospace (numbers/amounts)**: JetBrains Mono or Tabular Outfit — aligned columns for financial data + +### Spatial Rules +- Cards have generous internal padding but tight grid gaps — dense layout, breathable cards +- Consistent card anatomy: title → key metric → status indicator → primary action +- Left-border color accent for urgency (4px solid) +- Rounded corners (8-12px) on all cards — Apple softness + +## Layout Architecture + +### Widget-Based Grid +- CSS Grid dashboard with named areas +- Draggable, resizable panels (saved to user preferences in KV) +- Auto-personalizes: highest-urgency widgets float to top-left +- Responsive: collapses to single-column on mobile + +### Sections +1. **Top status bar** — cash position, next due date, sync freshness dots (green/amber/red), Focus Mode toggle +2. **Sidebar** (dark) — navigation, account summary, quick filters +3. **Main grid** — widget cards: + - Obligations (bills due, sorted by urgency) + - Active disputes (progress bars, status, next action) + - Cashflow chart (30-day projection) + - Recommendations (AI-scored, one CTA each) + - Recent transactions (last 10, grouped by account) + - Upcoming deadlines (calendar-style, next 14 days) + - Sync status (per-source freshness, last run time) + +## ADHD/Neurospicy-First Principles + +### 1. Focus Mode (default: ON) +- Landing view shows only top 3 most urgent items +- Each item: what it is, why it's urgent, one action button +- "See everything" toggle expands to full dense dashboard +- Reduces cognitive load on every visit — no overwhelm on load + +### 2. Visual Hierarchy Does the Thinking +- Urgency scoring auto-sorts all widgets and items within widgets +- Most important = biggest, brightest, top-left position +- No scanning or deciding "what should I look at first" + +### 3. One Clear Action Per Card +- Single primary CTA button per card (pay, respond, review) +- Secondary actions behind a "..." menu +- No decision paralysis from multiple equal-weight options + +### 4. Color-Coded Urgency Borders +- Red (4px left border): overdue or due within 48hrs +- Amber: due within 7 days or needs attention +- Green: on track, no action needed +- Peripheral vision catches urgency without reading + +### 5. Progress Indicators Everywhere +- Disputes: progress bar (filed → response → resolution) +- Monthly bills: "3 of 7 paid" with filled dots +- Sync cycles: completion indicators per source +- Dopamine-friendly — visible forward motion + +### 6. Chunked Sections with Clear Labels +- Bold section headers, clear card boundaries +- No wall-of-data — every group is visually separated +- Consistent card anatomy reduces cognitive parsing + +### 7. Muted Non-Urgent Items +- Low-urgency cards: lighter text, no accent border, smaller font +- High-urgency: full color, accent border, larger metric +- Attention goes where it's needed without effort + +### 8. Persistent Widget Layout +- Drag to reorder, collapse/expand — arrangement saved +- Same layout every visit — no re-orienting +- Layout stored in Cloudflare KV per user + +## Subtle Data Indicators (Not Animations) + +- **Freshness dots**: small colored circles (green=fresh, amber=stale, red=failed) next to each data source +- **Delta arrows**: small up/down arrows on amounts showing change from last sync +- **Muted timestamps**: "2h ago" in light gray under each card +- No pulse effects, no ticker strips, no glow — data confidence without noise + +## Tech Stack + +- **Framework**: React (existing) + Shadcn UI components +- **Styling**: Tailwind CSS with CSS custom properties for theming +- **Grid**: CSS Grid with `react-grid-layout` for drag/resize +- **Charts**: Recharts (lightweight, React-native) +- **State**: React context + Cloudflare KV for layout persistence +- **Build**: Vite → Cloudflare Pages + +## Not Included + +- Ticker strips / scrolling tapes +- Scan-line or CRT effects +- Heavy animations or glow effects +- Sound effects or haptics +- Multiple theme options (single cohesive theme) diff --git a/docs/plans/2026-02-23-ui-overhaul-plan.md b/docs/plans/2026-02-23-ui-overhaul-plan.md new file mode 100644 index 0000000..6d6240f --- /dev/null +++ b/docs/plans/2026-02-23-ui-overhaul-plan.md @@ -0,0 +1,1457 @@ +# ChittyCommand UI Overhaul — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Transform ChittyCommand from a dark-theme developer dashboard into an ADHD-friendly "Command Console" with dark chrome shell, light card surfaces, Focus Mode, urgency-based hierarchy, and widget-based layout. + +**Architecture:** Incremental replacement of existing UI. Theme foundation first, then shared components, then Layout shell, then Focus Mode, then page-by-page overhaul. Each task produces a working state — no broken intermediate builds. + +**Tech Stack:** React 18, Tailwind CSS 3, Vite 5, react-grid-layout (new), recharts (new), Outfit + JetBrains Mono fonts (new), lucide-react (existing) + +**Design Doc:** `docs/plans/2026-02-23-ui-overhaul-design.md` + +--- + +### Task 1: Install Dependencies & Font Setup + +**Files:** +- Modify: `ui/package.json` +- Modify: `ui/index.html` +- Modify: `ui/src/index.css` + +**Step 1: Install new packages** + +Run: `cd /Users/nb/Desktop/Projects/github.com/CHITTYOS/chittycommand/ui && npm install react-grid-layout recharts` +Run: `cd /Users/nb/Desktop/Projects/github.com/CHITTYOS/chittycommand/ui && npm install -D @types/react-grid-layout` + +**Step 2: Add Google Fonts to index.html** + +In `ui/index.html`, add to ``: + +```html + + + +``` + +**Step 3: Update index.css with new base styles** + +Replace `ui/src/index.css` with: + +```css +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + /* Chrome (dark shell) */ + --chrome-bg: #1a1a2e; + --chrome-surface: #16213e; + --chrome-border: #2a2a4a; + --chrome-text: #e2e8f0; + --chrome-text-muted: #94a3b8; + + /* Cards (light surfaces) */ + --card-bg: #ffffff; + --card-bg-hover: #f8fafc; + --card-border: #e2e8f0; + --card-text: #1e293b; + --card-text-muted: #64748b; + + /* Urgency */ + --urgency-red: #ef4444; + --urgency-amber: #f59e0b; + --urgency-green: #22c55e; + --urgency-red-bg: #fef2f2; + --urgency-amber-bg: #fffbeb; + --urgency-green-bg: #f0fdf4; + + /* Brand */ + --chitty-500: #4c6ef5; + --chitty-600: #3b5bdb; + --chitty-700: #364fc7; +} + +body { + margin: 0; + font-family: 'Outfit', -apple-system, BlinkMacSystemFont, sans-serif; + background-color: var(--chrome-bg); + color: var(--chrome-text); + -webkit-font-smoothing: antialiased; +} + +/* Monospace for financial numbers */ +.font-mono { + font-family: 'JetBrains Mono', ui-monospace, monospace; +} + +/* react-grid-layout overrides */ +.react-grid-item.react-grid-placeholder { + background: var(--chitty-500) !important; + opacity: 0.15 !important; + border-radius: 12px !important; +} +``` + +**Step 4: Verify build** + +Run: `cd /Users/nb/Desktop/Projects/github.com/CHITTYOS/chittycommand/ui && npx vite build` +Expected: Build succeeds with no errors. + +**Step 5: Commit** + +```bash +git add ui/package.json ui/package-lock.json ui/index.html ui/src/index.css +git commit -m "feat(ui): add Outfit/JetBrains Mono fonts, recharts, react-grid-layout" +``` + +--- + +### Task 2: Tailwind Theme Config + +**Files:** +- Modify: `ui/tailwind.config.js` + +**Step 1: Replace tailwind.config.js** + +```js +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: { + fontFamily: { + sans: ['Outfit', '-apple-system', 'BlinkMacSystemFont', 'sans-serif'], + mono: ['JetBrains Mono', 'ui-monospace', 'monospace'], + }, + colors: { + chitty: { + 50: '#f0f4ff', + 100: '#dbe4ff', + 500: '#4c6ef5', + 600: '#3b5bdb', + 700: '#364fc7', + 900: '#1b2559', + }, + chrome: { + bg: '#1a1a2e', + surface: '#16213e', + border: '#2a2a4a', + text: '#e2e8f0', + muted: '#94a3b8', + }, + card: { + bg: '#ffffff', + hover: '#f8fafc', + border: '#e2e8f0', + text: '#1e293b', + muted: '#64748b', + }, + urgency: { + red: '#ef4444', + amber: '#f59e0b', + green: '#22c55e', + }, + }, + borderRadius: { + card: '12px', + }, + }, + }, + plugins: [], +}; +``` + +**Step 2: Verify build** + +Run: `cd /Users/nb/Desktop/Projects/github.com/CHITTYOS/chittycommand/ui && npx vite build` +Expected: Build succeeds. + +**Step 3: Commit** + +```bash +git add ui/tailwind.config.js +git commit -m "feat(ui): extend Tailwind theme with Command Console color system" +``` + +--- + +### Task 3: Shared UI Components + +**Files:** +- Create: `ui/src/components/ui/Card.tsx` +- Create: `ui/src/components/ui/UrgencyBorder.tsx` +- Create: `ui/src/components/ui/FreshnessDot.tsx` +- Create: `ui/src/components/ui/ProgressDots.tsx` +- Create: `ui/src/components/ui/MetricCard.tsx` +- Create: `ui/src/components/ui/ActionButton.tsx` + +**Step 1: Create Card component** + +`ui/src/components/ui/Card.tsx`: + +```tsx +import { cn } from '../../lib/utils'; + +interface CardProps { + children: React.ReactNode; + className?: string; + urgency?: 'red' | 'amber' | 'green' | null; + muted?: boolean; + onClick?: () => void; +} + +export function Card({ children, className, urgency, muted, onClick }: CardProps) { + const borderColor = urgency === 'red' + ? 'border-l-urgency-red' + : urgency === 'amber' + ? 'border-l-urgency-amber' + : urgency === 'green' + ? 'border-l-urgency-green' + : 'border-l-transparent'; + + return ( +
+ {children} +
+ ); +} +``` + +**Step 2: Create UrgencyBorder helper** + +`ui/src/components/ui/UrgencyBorder.tsx`: + +```tsx +export function urgencyLevel(score: number | null): 'red' | 'amber' | 'green' | null { + if (score === null || score === undefined) return null; + if (score >= 70) return 'red'; + if (score >= 40) return 'amber'; + return 'green'; +} + +export function urgencyFromDays(days: number): 'red' | 'amber' | 'green' { + if (days <= 2) return 'red'; + if (days <= 7) return 'amber'; + return 'green'; +} +``` + +**Step 3: Create FreshnessDot** + +`ui/src/components/ui/FreshnessDot.tsx`: + +```tsx +import { cn } from '../../lib/utils'; + +interface FreshnessDotProps { + status: 'fresh' | 'stale' | 'failed' | 'unknown'; + className?: string; +} + +export function FreshnessDot({ status, className }: FreshnessDotProps) { + const color = status === 'fresh' + ? 'bg-urgency-green' + : status === 'stale' + ? 'bg-urgency-amber' + : status === 'failed' + ? 'bg-urgency-red' + : 'bg-chrome-muted'; + + return ; +} + +export function freshnessFromDate(dateStr: string | null): 'fresh' | 'stale' | 'failed' | 'unknown' { + if (!dateStr) return 'unknown'; + const hours = (Date.now() - new Date(dateStr).getTime()) / (1000 * 60 * 60); + if (hours < 24) return 'fresh'; + if (hours < 72) return 'stale'; + return 'failed'; +} +``` + +**Step 4: Create ProgressDots** + +`ui/src/components/ui/ProgressDots.tsx`: + +```tsx +import { cn } from '../../lib/utils'; + +interface ProgressDotsProps { + completed: number; + total: number; + className?: string; +} + +export function ProgressDots({ completed, total, className }: ProgressDotsProps) { + return ( +
+ {Array.from({ length: total }, (_, i) => ( + + ))} + + {completed}/{total} + +
+ ); +} +``` + +**Step 5: Create MetricCard** + +`ui/src/components/ui/MetricCard.tsx`: + +```tsx +import { cn } from '../../lib/utils'; + +interface MetricCardProps { + label: string; + value: string; + trend?: 'up' | 'down' | null; + className?: string; + valueClassName?: string; +} + +export function MetricCard({ label, value, trend, className, valueClassName }: MetricCardProps) { + return ( +
+

{label}

+
+

{value}

+ {trend === 'up' && } + {trend === 'down' && } +
+
+ ); +} +``` + +**Step 6: Create ActionButton** + +`ui/src/components/ui/ActionButton.tsx`: + +```tsx +import { cn } from '../../lib/utils'; + +interface ActionButtonProps { + label: string; + onClick: () => void; + variant?: 'primary' | 'secondary' | 'danger'; + loading?: boolean; + disabled?: boolean; + className?: string; +} + +export function ActionButton({ label, onClick, variant = 'primary', loading, disabled, className }: ActionButtonProps) { + const base = 'px-4 py-2 text-sm font-medium rounded-lg transition-colors disabled:opacity-50'; + const variants = { + primary: 'bg-chitty-600 text-white hover:bg-chitty-700', + secondary: 'bg-card-border text-card-text hover:bg-gray-200', + danger: 'bg-urgency-red text-white hover:bg-red-600', + }; + + return ( + + ); +} +``` + +**Step 7: Verify build** + +Run: `cd /Users/nb/Desktop/Projects/github.com/CHITTYOS/chittycommand/ui && npx vite build` +Expected: Build succeeds (components not yet used, but must compile). + +**Step 8: Commit** + +```bash +git add ui/src/components/ui/ +git commit -m "feat(ui): add shared Card, MetricCard, urgency, freshness, progress components" +``` + +--- + +### Task 4: New Layout — Sidebar + Status Bar + +**Files:** +- Modify: `ui/src/components/Layout.tsx` +- Create: `ui/src/components/Sidebar.tsx` +- Create: `ui/src/components/StatusBar.tsx` +- Create: `ui/src/lib/focus-mode.tsx` + +**Step 1: Create FocusMode context** + +`ui/src/lib/focus-mode.tsx`: + +```tsx +import { createContext, useContext, useState, useCallback, type ReactNode } from 'react'; + +interface FocusModeContextType { + focusMode: boolean; + toggleFocusMode: () => void; +} + +const FocusModeContext = createContext({ + focusMode: true, + toggleFocusMode: () => {}, +}); + +export function FocusModeProvider({ children }: { children: ReactNode }) { + const [focusMode, setFocusMode] = useState(() => { + const saved = localStorage.getItem('chittycommand_focus_mode'); + return saved !== null ? saved === 'true' : true; // default ON + }); + + const toggleFocusMode = useCallback(() => { + setFocusMode((prev) => { + const next = !prev; + localStorage.setItem('chittycommand_focus_mode', String(next)); + return next; + }); + }, []); + + return ( + + {children} + + ); +} + +export function useFocusMode() { + return useContext(FocusModeContext); +} +``` + +**Step 2: Create Sidebar** + +`ui/src/components/Sidebar.tsx`: + +```tsx +import { NavLink } from 'react-router-dom'; +import { cn } from '../lib/utils'; +import { + LayoutDashboard, Receipt, ShieldAlert, Wallet, Scale, + Lightbulb, TrendingUp, Upload, Settings, LogOut, +} from 'lucide-react'; +import { logout, getUser } from '../lib/auth'; + +const navItems = [ + { path: '/', label: 'Dashboard', icon: LayoutDashboard }, + { path: '/bills', label: 'Bills', icon: Receipt }, + { path: '/disputes', label: 'Disputes', icon: ShieldAlert }, + { path: '/accounts', label: 'Accounts', icon: Wallet }, + { path: '/legal', label: 'Legal', icon: Scale }, + { path: '/recommendations', label: 'AI Recs', icon: Lightbulb }, + { path: '/cashflow', label: 'Cash Flow', icon: TrendingUp }, + { path: '/upload', label: 'Upload', icon: Upload }, + { path: '/settings', label: 'Settings', icon: Settings }, +]; + +export function Sidebar() { + const user = getUser(); + + return ( + + ); +} +``` + +**Step 3: Create StatusBar** + +`ui/src/components/StatusBar.tsx`: + +```tsx +import { useFocusMode } from '../lib/focus-mode'; +import { Eye, EyeOff } from 'lucide-react'; + +interface StatusBarProps { + cashPosition?: string; + nextDue?: string; +} + +export function StatusBar({ cashPosition, nextDue }: StatusBarProps) { + const { focusMode, toggleFocusMode } = useFocusMode(); + + return ( +
+
+ {cashPosition && ( +
+ Cash + {cashPosition} +
+ )} + {nextDue && ( +
+ Next Due + {nextDue} +
+ )} +
+ + +
+ ); +} +``` + +**Step 4: Rewrite Layout.tsx** + +Replace `ui/src/components/Layout.tsx`: + +```tsx +import { Outlet } from 'react-router-dom'; +import { Sidebar } from './Sidebar'; +import { StatusBar } from './StatusBar'; + +export function Layout() { + return ( +
+ +
+ +
+ +
+
+
+ ); +} +``` + +**Step 5: Wrap app with FocusModeProvider** + +In `ui/src/main.tsx`, wrap `` with ``: + +```tsx +import { FocusModeProvider } from './lib/focus-mode'; + +// Inside render: + + + + {/* ... routes ... */} + + + +``` + +**Step 6: Verify build and visual** + +Run: `cd /Users/nb/Desktop/Projects/github.com/CHITTYOS/chittycommand/ui && npx vite build` +Expected: Build succeeds. When running dev server, sidebar + status bar are visible. Pages still render but with old card colors (mixed state is OK — we'll fix pages next). + +**Step 7: Commit** + +```bash +git add ui/src/components/Layout.tsx ui/src/components/Sidebar.tsx ui/src/components/StatusBar.tsx ui/src/lib/focus-mode.tsx ui/src/main.tsx +git commit -m "feat(ui): new sidebar + status bar layout with Focus Mode context" +``` + +--- + +### Task 5: Dashboard Overhaul — Focus Mode + Full Mode + +This is the biggest task. The Dashboard page gets completely rewritten with Focus Mode (default ON showing top 3 items) and Full Mode (dense widget grid). + +**Files:** +- Modify: `ui/src/pages/Dashboard.tsx` +- Create: `ui/src/components/dashboard/FocusView.tsx` +- Create: `ui/src/components/dashboard/FullView.tsx` +- Create: `ui/src/components/dashboard/ObligationsWidget.tsx` +- Create: `ui/src/components/dashboard/DisputesWidget.tsx` +- Create: `ui/src/components/dashboard/DeadlinesWidget.tsx` +- Create: `ui/src/components/dashboard/RecommendationsWidget.tsx` + +**Step 1: Create FocusView** + +`ui/src/components/dashboard/FocusView.tsx`: + +```tsx +import { Card } from '../ui/Card'; +import { ActionButton } from '../ui/ActionButton'; +import { urgencyLevel } from '../ui/UrgencyBorder'; +import type { DashboardData, Obligation, Recommendation } from '../../lib/api'; +import { formatCurrency, formatDate, daysUntil } from '../../lib/utils'; + +interface FocusViewProps { + data: DashboardData; + onPayNow: (ob: Obligation) => void; + onExecute: (rec: Recommendation) => void; + payingId: string | null; + executingId: string | null; +} + +interface FocusItem { + type: 'obligation' | 'dispute' | 'deadline' | 'recommendation'; + urgency: number; + title: string; + subtitle: string; + metric: string; + action: { label: string; onClick: () => void; loading: boolean }; +} + +export function FocusView({ data, onPayNow, onExecute, payingId, executingId }: FocusViewProps) { + const { obligations, disputes, deadlines, recommendations } = data; + + // Gather all items with urgency scores, pick top 3 + const items: FocusItem[] = []; + + obligations.urgent.forEach((ob) => { + items.push({ + type: 'obligation', + urgency: ob.urgency_score ?? 0, + title: ob.payee, + subtitle: ob.status === 'overdue' + ? `OVERDUE ${Math.abs(daysUntil(ob.due_date))} days` + : `Due ${formatDate(ob.due_date)}`, + metric: formatCurrency(ob.amount_due), + action: { + label: 'Pay Now', + onClick: () => onPayNow(ob), + loading: payingId === ob.id, + }, + }); + }); + + disputes.forEach((d) => { + items.push({ + type: 'dispute', + urgency: (6 - d.priority) * 20, // P1 = 100, P2 = 80, etc. + title: d.title, + subtitle: `vs ${d.counterparty}`, + metric: d.amount_at_stake ? formatCurrency(d.amount_at_stake) : '', + action: { + label: d.next_action ? 'Take Action' : 'View', + onClick: () => window.location.href = '/disputes', + loading: false, + }, + }); + }); + + deadlines.forEach((dl) => { + const days = daysUntil(dl.deadline_date); + items.push({ + type: 'deadline', + urgency: dl.urgency_score ?? (days <= 7 ? 80 : 30), + title: dl.title, + subtitle: dl.case_ref, + metric: days > 0 ? `${days}d left` : days === 0 ? 'TODAY' : `${Math.abs(days)}d ago`, + action: { + label: 'View', + onClick: () => window.location.href = '/legal', + loading: false, + }, + }); + }); + + recommendations.slice(0, 3).forEach((rec) => { + items.push({ + type: 'recommendation', + urgency: (6 - rec.priority) * 15, + title: rec.title, + subtitle: rec.reasoning, + metric: '', + action: { + label: rec.action_type ? 'Execute' : 'View', + onClick: () => rec.action_type ? onExecute(rec) : (window.location.href = '/recommendations'), + loading: executingId === rec.id, + }, + }); + }); + + // Sort by urgency descending, take top 3 + const top3 = items.sort((a, b) => b.urgency - a.urgency).slice(0, 3); + + if (top3.length === 0) { + return ( +
+
+

All clear

+

Nothing urgent right now.

+
+
+ ); + } + + return ( +
+

Needs your attention

+ {top3.map((item, i) => ( + +
+

{item.title}

+

{item.subtitle}

+
+ {item.metric && ( +

{item.metric}

+ )} + +
+ ))} +
+ ); +} +``` + +**Step 2: Create ObligationsWidget** + +`ui/src/components/dashboard/ObligationsWidget.tsx`: + +```tsx +import { Card } from '../ui/Card'; +import { urgencyLevel } from '../ui/UrgencyBorder'; +import type { Obligation } from '../../lib/api'; +import { formatCurrency, formatDate, daysUntil } from '../../lib/utils'; + +interface Props { + obligations: Obligation[]; + onPayNow: (ob: Obligation) => void; + payingId: string | null; +} + +export function ObligationsWidget({ obligations, onPayNow, payingId }: Props) { + if (obligations.length === 0) { + return ( +
+

Upcoming Bills

+

No pending obligations

+
+ ); + } + + return ( +
+

Upcoming Bills

+ {obligations.map((ob) => { + const days = daysUntil(ob.due_date); + return ( + +
+
+

{ob.payee}

+

+ {ob.category} — {ob.status === 'overdue' + ? `${Math.abs(days)}d overdue` + : `Due ${formatDate(ob.due_date)}`} +

+
+
+

{formatCurrency(ob.amount_due)}

+ {ob.status !== 'paid' && ( + + )} +
+
+
+ ); + })} +
+ ); +} +``` + +**Step 3: Create DisputesWidget** + +`ui/src/components/dashboard/DisputesWidget.tsx`: + +```tsx +import { Card } from '../ui/Card'; +import { ProgressDots } from '../ui/ProgressDots'; +import type { Dispute } from '../../lib/api'; +import { formatCurrency } from '../../lib/utils'; + +const DISPUTE_STAGES = ['filed', 'response_pending', 'in_review', 'resolved']; + +function disputeStageIndex(status: string): number { + const idx = DISPUTE_STAGES.indexOf(status); + return idx >= 0 ? idx : 0; +} + +interface Props { + disputes: Dispute[]; +} + +export function DisputesWidget({ disputes }: Props) { + if (disputes.length === 0) { + return ( +
+

Active Disputes

+

No active disputes

+
+ ); + } + + return ( +
+

Active Disputes

+ {disputes.map((d) => ( + +
+
+

{d.title}

+

vs {d.counterparty}

+ +
+
+ {d.amount_at_stake && ( +

{formatCurrency(d.amount_at_stake)}

+ )} + {d.next_action && ( + {d.next_action} + )} +
+
+
+ ))} +
+ ); +} +``` + +**Step 4: Create DeadlinesWidget** + +`ui/src/components/dashboard/DeadlinesWidget.tsx`: + +```tsx +import { Card } from '../ui/Card'; +import { urgencyFromDays } from '../ui/UrgencyBorder'; +import type { LegalDeadline } from '../../lib/api'; +import { formatDate, daysUntil } from '../../lib/utils'; + +interface Props { + deadlines: LegalDeadline[]; +} + +export function DeadlinesWidget({ deadlines }: Props) { + if (deadlines.length === 0) return null; + + return ( +
+

Legal Deadlines

+ {deadlines.map((dl) => { + const days = daysUntil(dl.deadline_date); + return ( + +
+
+

{dl.title}

+

{dl.case_ref}

+
+
+

+ {days > 0 ? `${days}d` : days === 0 ? 'TODAY' : `${Math.abs(days)}d ago`} +

+

{formatDate(dl.deadline_date)}

+
+
+
+ ); + })} +
+ ); +} +``` + +**Step 5: Create RecommendationsWidget** + +`ui/src/components/dashboard/RecommendationsWidget.tsx`: + +```tsx +import { Card } from '../ui/Card'; +import { ActionButton } from '../ui/ActionButton'; +import type { Recommendation } from '../../lib/api'; + +interface Props { + recommendations: Recommendation[]; + onExecute: (rec: Recommendation) => void; + executingId: string | null; +} + +export function RecommendationsWidget({ recommendations, onExecute, executingId }: Props) { + if (recommendations.length === 0) return null; + + return ( +
+

AI Recommendations

+ {recommendations.map((rec) => ( + +
+
+
+ {rec.rec_type} +
+

{rec.title}

+

{rec.reasoning}

+
+ {rec.action_type && ( + onExecute(rec)} + loading={executingId === rec.id} + className="shrink-0" + /> + )} +
+
+ ))} +
+ ); +} +``` + +**Step 6: Create FullView** + +`ui/src/components/dashboard/FullView.tsx`: + +```tsx +import { MetricCard } from '../ui/MetricCard'; +import { ObligationsWidget } from './ObligationsWidget'; +import { DisputesWidget } from './DisputesWidget'; +import { DeadlinesWidget } from './DeadlinesWidget'; +import { RecommendationsWidget } from './RecommendationsWidget'; +import type { DashboardData, Obligation, Recommendation } from '../../lib/api'; +import { formatCurrency } from '../../lib/utils'; + +interface FullViewProps { + data: DashboardData; + onPayNow: (ob: Obligation) => void; + onExecute: (rec: Recommendation) => void; + payingId: string | null; + executingId: string | null; +} + +export function FullView({ data, onPayNow, onExecute, payingId, executingId }: FullViewProps) { + const { summary, obligations, disputes, deadlines, recommendations } = data; + + return ( +
+ {/* Summary Metrics */} +
+ + + + 0 ? 'text-urgency-red' : 'text-urgency-green'} + /> +
+ + {/* Two-column widget grid */} +
+ + +
+ +
+ + +
+
+ ); +} +``` + +**Step 7: Rewrite Dashboard.tsx** + +Replace `ui/src/pages/Dashboard.tsx`: + +```tsx +import { useEffect, useState, useCallback } from 'react'; +import { api, type DashboardData, type Obligation, type Recommendation } from '../lib/api'; +import { useFocusMode } from '../lib/focus-mode'; +import { FocusView } from '../components/dashboard/FocusView'; +import { FullView } from '../components/dashboard/FullView'; + +export function Dashboard() { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [payingId, setPayingId] = useState(null); + const [executingId, setExecutingId] = useState(null); + const { focusMode } = useFocusMode(); + + const reload = useCallback(() => { + api.getDashboard().then(setData).catch((e) => setError(e.message)); + }, []); + + useEffect(() => { reload(); }, [reload]); + + const handlePayNow = async (ob: Obligation) => { + if (payingId) return; + setPayingId(ob.id); + try { + await api.markPaid(ob.id); + reload(); + } catch (e: unknown) { + setError(e instanceof Error ? e.message : 'Payment failed'); + } finally { + setPayingId(null); + } + }; + + const handleExecute = async (rec: Recommendation) => { + if (executingId) return; + setExecutingId(rec.id); + try { + await api.actOnRecommendation(rec.id, { action_taken: rec.action_type || 'executed' }); + reload(); + } catch (e: unknown) { + setError(e instanceof Error ? e.message : 'Execution failed'); + } finally { + setExecutingId(null); + } + }; + + if (error && !data) { + return ( +
+

Failed to load dashboard

+

{error}

+
+ ); + } + + if (!data) { + return
Loading...
; + } + + const viewProps = { data, onPayNow: handlePayNow, onExecute: handleExecute, payingId, executingId }; + + return focusMode ? : ; +} +``` + +**Step 8: Verify build** + +Run: `cd /Users/nb/Desktop/Projects/github.com/CHITTYOS/chittycommand/ui && npx vite build` +Expected: Build succeeds. + +**Step 9: Commit** + +```bash +git add ui/src/pages/Dashboard.tsx ui/src/components/dashboard/ +git commit -m "feat(ui): dashboard overhaul with Focus Mode + full widget grid" +``` + +--- + +### Task 6: Bills Page Redesign + +**Files:** +- Modify: `ui/src/pages/Bills.tsx` + +**Step 1: Rewrite Bills.tsx** + +Replace with card-based layout using new components. Key changes: +- Replace dark table with Card components per obligation +- Urgency left-border on each card +- Muted low-urgency items +- Keep filter buttons but restyle for light cards on dark chrome + +```tsx +import { useEffect, useState } from 'react'; +import { api, type Obligation } from '../lib/api'; +import { Card } from '../components/ui/Card'; +import { ActionButton } from '../components/ui/ActionButton'; +import { urgencyLevel } from '../components/ui/UrgencyBorder'; +import { formatCurrency, formatDate, daysUntil } from '../lib/utils'; +import { cn } from '../lib/utils'; + +export function Bills() { + const [obligations, setObligations] = useState([]); + const [filter, setFilter] = useState(''); + const [error, setError] = useState(null); + const [payingId, setPayingId] = useState(null); + + useEffect(() => { + const params: Record = {}; + if (filter) params.status = filter; + api.getObligations(params).then(setObligations).catch((e) => setError(e.message)); + }, [filter]); + + const handleMarkPaid = async (id: string) => { + setPayingId(id); + try { + await api.markPaid(id); + setObligations((prev) => prev.map((o) => (o.id === id ? { ...o, status: 'paid', urgency_score: 0 } : o))); + } finally { + setPayingId(null); + } + }; + + if (error) return

{error}

; + + const filters = ['', 'pending', 'overdue', 'paid']; + + return ( +
+
+

Bills & Obligations

+
+ {filters.map((f) => ( + + ))} +
+
+ +
+ {obligations.map((ob) => { + const days = ob.due_date ? daysUntil(ob.due_date) : null; + return ( + +
+
+

{ob.payee}

+

+ {ob.category} + {ob.due_date && ( + <> — {days !== null && days < 0 + ? {Math.abs(days)}d late + : days === 0 + ? Due today + : `Due ${formatDate(ob.due_date)}` + } + )} +

+
+
+

{formatCurrency(ob.amount_due)}

+ + {ob.status} + + {ob.status !== 'paid' && ( + handleMarkPaid(ob.id)} + loading={payingId === ob.id} + /> + )} +
+
+
+ ); + })} + {obligations.length === 0 && ( +

No obligations found

+ )} +
+
+ ); +} +``` + +**Step 2: Verify build** + +Run: `cd /Users/nb/Desktop/Projects/github.com/CHITTYOS/chittycommand/ui && npx vite build` +Expected: Build succeeds. + +**Step 3: Commit** + +```bash +git add ui/src/pages/Bills.tsx +git commit -m "feat(ui): redesign Bills page with urgency cards and ADHD-friendly layout" +``` + +--- + +### Task 7: Disputes Page Redesign + +**Files:** +- Modify: `ui/src/pages/Disputes.tsx` + +**Step 1: Rewrite Disputes.tsx with progress bars and one-action-per-card** + +Key changes: +- Card component with urgency borders based on priority +- Progress bar showing dispute stage +- Single primary CTA per card +- Correspondence/documents behind expandable panel +- Use new Card, ProgressDots, ActionButton components + +Keep the existing expand/collapse logic for correspondence and documents panels but restyle with light card surfaces. + +**Step 2: Verify build** + +Run: `cd /Users/nb/Desktop/Projects/github.com/CHITTYOS/chittycommand/ui && npx vite build` + +**Step 3: Commit** + +```bash +git add ui/src/pages/Disputes.tsx +git commit -m "feat(ui): redesign Disputes page with progress bars and one-action-per-card" +``` + +--- + +### Task 8: Accounts Page Redesign + +**Files:** +- Modify: `ui/src/pages/Accounts.tsx` + +**Step 1: Rewrite with new Card components** + +Key changes: +- Group headers use chrome-text styling +- Account cards use Card component with light surfaces +- Credit utilization bar uses Tailwind classes matching new palette +- Balance text uses font-mono with urgency colors + +**Step 2: Verify build and commit** + +```bash +git add ui/src/pages/Accounts.tsx +git commit -m "feat(ui): redesign Accounts page with light cards and new color system" +``` + +--- + +### Task 9: CashFlow Page Redesign + +**Files:** +- Modify: `ui/src/pages/CashFlow.tsx` + +**Step 1: Replace custom bar chart with Recharts AreaChart** + +Key changes: +- Use `recharts` `` with fill gradient for cash flow projection +- Keep scenario panel but restyle with Card/MetricCard components +- Outflows table uses light card surface +- Restyle all buttons with ActionButton + +**Step 2: Verify build and commit** + +```bash +git add ui/src/pages/CashFlow.tsx +git commit -m "feat(ui): redesign CashFlow page with Recharts and new card system" +``` + +--- + +### Task 10: Remaining Pages — Settings, Login, Recommendations, Legal, Upload + +**Files:** +- Modify: `ui/src/pages/Settings.tsx` +- Modify: `ui/src/pages/Login.tsx` +- Modify: `ui/src/pages/Recommendations.tsx` +- Modify: `ui/src/pages/Legal.tsx` +- Modify: `ui/src/pages/Upload.tsx` + +**Step 1: Settings page** + +Restyle all tables and panels with light card surfaces. Service cards use Card component. Sync status table uses Card-based rows. + +**Step 2: Login page** + +Dark background stays (login is outside Layout). Restyle form card with rounded-card, update input fields to use chrome colors, add Outfit font to title. + +**Step 3: Recommendations page** + +Use Card components with urgency borders. One CTA per card. Priority badges use pill style on light backgrounds. + +**Step 4: Legal page** + +Apply Card components with urgencyFromDays borders. Same pattern as DeadlinesWidget. + +**Step 5: Upload page** + +Restyle upload area with Card + dashed border on light surface. + +**Step 6: Verify full build** + +Run: `cd /Users/nb/Desktop/Projects/github.com/CHITTYOS/chittycommand/ui && npx vite build` +Expected: Clean build, no TypeScript errors. + +**Step 7: Commit** + +```bash +git add ui/src/pages/Settings.tsx ui/src/pages/Login.tsx ui/src/pages/Recommendations.tsx ui/src/pages/Legal.tsx ui/src/pages/Upload.tsx +git commit -m "feat(ui): redesign Settings, Login, Recommendations, Legal, Upload pages" +``` + +--- + +### Task 11: StatusBar Live Data + Polish + +**Files:** +- Modify: `ui/src/components/Layout.tsx` +- Modify: `ui/src/components/StatusBar.tsx` + +**Step 1: Pipe dashboard summary data into StatusBar** + +The Layout component needs to fetch summary data and pass it to StatusBar. Add a lightweight API call in Layout that fetches `/api/dashboard` summary (cash position, next due date) and passes to StatusBar props. + +**Step 2: Add freshness dots to StatusBar** + +Add sync status freshness dots to the status bar — small colored dots per data source showing last sync freshness. + +**Step 3: Verify and commit** + +```bash +git add ui/src/components/Layout.tsx ui/src/components/StatusBar.tsx +git commit -m "feat(ui): wire live data into StatusBar with freshness indicators" +``` + +--- + +### Task 12: Final Visual Polish & Build Verification + +**Files:** +- Various minor tweaks across all pages + +**Step 1: Run full build** + +Run: `cd /Users/nb/Desktop/Projects/github.com/CHITTYOS/chittycommand/ui && npx vite build` +Expected: Clean build. + +**Step 2: Run dev server and visually verify each page** + +Run: `cd /Users/nb/Desktop/Projects/github.com/CHITTYOS/chittycommand/ui && npx vite --host` + +Verify: +- [ ] Login page renders with new styling +- [ ] Dashboard Focus Mode shows top 3 urgent items +- [ ] Focus Mode toggle switches to full view +- [ ] Full dashboard shows 4 metric cards + widget grid +- [ ] Bills page shows urgency-bordered cards +- [ ] Disputes page shows progress dots +- [ ] Accounts page shows grouped cards +- [ ] CashFlow page shows Recharts chart +- [ ] Settings page shows light card tables +- [ ] Sidebar navigation works for all routes +- [ ] Status bar shows cash position + +**Step 3: Fix any visual issues found** + +Address spacing, color, or typography inconsistencies. + +**Step 4: Final commit** + +```bash +git add -A +git commit -m "feat(ui): Command Console visual polish and build verification" +``` + +--- + +### Dependency Graph + +``` +Task 1 (fonts/deps) ──┐ + ├── Task 3 (shared components) +Task 2 (tailwind) ────┘ │ + ├── Task 4 (layout + focus mode) + │ │ + │ ├── Task 5 (dashboard) + │ ├── Task 6 (bills) + │ ├── Task 7 (disputes) + │ ├── Task 8 (accounts) + │ ├── Task 9 (cashflow) + │ ├── Task 10 (remaining pages) + │ └── Task 11 (statusbar data) + │ │ + │ └── Task 12 (polish) +``` + +Tasks 5-10 can be parallelized (independent pages). Tasks 1-4 are sequential. diff --git a/package-lock.json b/package-lock.json index 5f39216..e24bf8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@cloudflare/workers-types": "^4.20240512.0", "drizzle-kit": "^0.24.0", "typescript": "^5.5.0", + "vitest": "^4.0.18", "wrangler": "^4.60.0" } }, @@ -1614,6 +1615,356 @@ "dev": true, "license": "MIT" }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@sindresorhus/is": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", @@ -1634,6 +1985,38 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "25.2.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.2.tgz", @@ -1654,21 +2037,152 @@ "pg-types": "^4.0.1" } }, - "node_modules/blake3-wasm": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", - "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "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==", + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "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/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", @@ -1692,553 +2206,1410 @@ "ms": "^2.1.3" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/drizzle-kit": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.24.2.tgz", + "integrity": "sha512-nXOaTSFiuIaTMhS8WJC2d4EBeIcN9OSt2A2cyFbQYBAZbi7lRsVGJNqDpEwPqYfJz38yxbY/UtbvBBahBfnExQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.1", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.19.7", + "esbuild-register": "^3.5.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.33.0.tgz", + "integrity": "sha512-SHy72R2Rdkz0LEq0PSG/IdvnT3nGiWuRk+2tXZQ90GVq/XQhpCzu/EFT3V2rox+w8MlkBQxifF8pCStNYnERfA==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=3", + "@electric-sql/pglite": ">=0.1.1", + "@libsql/client": "*", + "@neondatabase/serverless": ">=0.1", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/react": ">=18", + "@types/sql.js": "*", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=13.2.0", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "react": ">=18", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "react": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/hono": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", + "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/miniflare": { + "version": "4.20260219.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260219.0.tgz", + "integrity": "sha512-EIb5wXbWUnnC60XU2aiFOPNd4fgTXzECkwRSOXZ1vdcY9WZaEE9rVf+h+Apw+WkOHRkp3Dr9/ZhQ5y1R+9iZ4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "^0.34.5", + "undici": "7.18.2", + "workerd": "1.20260219.0", + "ws": "8.18.0", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-protocol": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", + "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.1.0.tgz", + "integrity": "sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.1.0", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "license": "MIT", + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postgres-date": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-range": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", + "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", + "license": "MIT" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "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/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "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/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", + "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" } }, - "node_modules/drizzle-kit": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.24.2.tgz", - "integrity": "sha512-nXOaTSFiuIaTMhS8WJC2d4EBeIcN9OSt2A2cyFbQYBAZbi7lRsVGJNqDpEwPqYfJz38yxbY/UtbvBBahBfnExQ==", + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "dependencies": { - "@drizzle-team/brocli": "^0.10.1", - "@esbuild-kit/esm-loader": "^2.5.5", - "esbuild": "^0.19.7", - "esbuild-register": "^3.5.0" + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { - "drizzle-kit": "bin.cjs" - } - }, - "node_modules/drizzle-orm": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.33.0.tgz", - "integrity": "sha512-SHy72R2Rdkz0LEq0PSG/IdvnT3nGiWuRk+2tXZQ90GVq/XQhpCzu/EFT3V2rox+w8MlkBQxifF8pCStNYnERfA==", - "license": "Apache-2.0", + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, "peerDependencies": { - "@aws-sdk/client-rds-data": ">=3", - "@cloudflare/workers-types": ">=3", - "@electric-sql/pglite": ">=0.1.1", - "@libsql/client": "*", - "@neondatabase/serverless": ">=0.1", - "@op-engineering/op-sqlite": ">=2", - "@opentelemetry/api": "^1.4.1", - "@planetscale/database": ">=1", - "@prisma/client": "*", - "@tidbcloud/serverless": "*", - "@types/better-sqlite3": "*", - "@types/pg": "*", - "@types/react": ">=18", - "@types/sql.js": "*", - "@vercel/postgres": ">=0.8.0", - "@xata.io/client": "*", - "better-sqlite3": ">=7", - "bun-types": "*", - "expo-sqlite": ">=13.2.0", - "knex": "*", - "kysely": "*", - "mysql2": ">=2", - "pg": ">=8", - "postgres": ">=3", - "react": ">=18", - "sql.js": ">=1", - "sqlite3": ">=5" + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { - "@aws-sdk/client-rds-data": { - "optional": true - }, - "@cloudflare/workers-types": { - "optional": true - }, - "@electric-sql/pglite": { - "optional": true - }, - "@libsql/client": { - "optional": true - }, - "@neondatabase/serverless": { - "optional": true - }, - "@op-engineering/op-sqlite": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@prisma/client": { - "optional": true - }, - "@tidbcloud/serverless": { - "optional": true - }, - "@types/better-sqlite3": { - "optional": true - }, - "@types/pg": { - "optional": true - }, - "@types/react": { - "optional": true - }, - "@types/sql.js": { - "optional": true - }, - "@vercel/postgres": { - "optional": true - }, - "@xata.io/client": { - "optional": true - }, - "better-sqlite3": { - "optional": true - }, - "bun-types": { + "@types/node": { "optional": true }, - "expo-sqlite": { + "jiti": { "optional": true }, - "knex": { + "less": { "optional": true }, - "kysely": { + "lightningcss": { "optional": true }, - "mysql2": { + "sass": { "optional": true }, - "pg": { + "sass-embedded": { "optional": true }, - "postgres": { + "stylus": { "optional": true }, - "prisma": { + "sugarss": { "optional": true }, - "react": { + "terser": { "optional": true }, - "sql.js": { + "tsx": { "optional": true }, - "sqlite3": { + "yaml": { "optional": true } } }, - "node_modules/error-stack-parser-es": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", - "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" } }, - "node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" + "node": ">=18" } }, - "node_modules/esbuild-register": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", - "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "peerDependencies": { - "esbuild": ">=0.12 <1" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], "dev": true, - "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=18" } }, - "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/hono": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", - "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=16.9.0" + "node": ">=18" } }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/miniflare": { - "version": "4.20260219.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260219.0.tgz", - "integrity": "sha512-EIb5wXbWUnnC60XU2aiFOPNd4fgTXzECkwRSOXZ1vdcY9WZaEE9rVf+h+Apw+WkOHRkp3Dr9/ZhQ5y1R+9iZ4Q==", + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "0.8.1", - "sharp": "^0.34.5", - "undici": "7.18.2", - "workerd": "1.20260219.0", - "ws": "8.18.0", - "youch": "4.1.0-beta.10" - }, - "bin": { - "miniflare": "bootstrap.js" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.0.0" + "node": ">=18" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "license": "MIT" - }, - "node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "MIT" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4.0.0" + "node": ">=18" } }, - "node_modules/pg-numeric": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", - "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", - "license": "ISC", + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/pg-protocol": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", - "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", - "license": "MIT" - }, - "node_modules/pg-types": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.1.0.tgz", - "integrity": "sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg==", + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "pg-numeric": "1.0.2", - "postgres-array": "~3.0.1", - "postgres-bytea": "~3.0.0", - "postgres-date": "~2.1.0", - "postgres-interval": "^3.0.0", - "postgres-range": "^1.1.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/postgres-array": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", - "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/postgres-bytea": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", - "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "obuf": "~1.1.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 6" + "node": ">=18" } }, - "node_modules/postgres-date": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", - "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/postgres-interval": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", - "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/postgres-range": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", - "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", - "license": "MIT" - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "node": ">=18" } }, - "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==", + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "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==", + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/supports-color": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", - "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "node_modules/vite/node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "dev": true, - "license": "Apache-2.0", + "hasInstallScript": true, + "license": "MIT", "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=14.17" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, - "node_modules/undici": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", - "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, "engines": { - "node": ">=20.18.1" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } } }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "license": "MIT" - }, - "node_modules/unenv": { - "version": "2.0.0-rc.24", - "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", - "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { - "pathe": "^2.0.3" + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" } }, "node_modules/workerd": { diff --git a/package.json b/package.json index d7bc6dc..415bc97 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@cloudflare/workers-types": "^4.20240512.0", "drizzle-kit": "^0.24.0", "typescript": "^5.5.0", + "vitest": "^4.0.18", "wrangler": "^4.60.0" } } diff --git a/src/lib/urgency.test.ts b/src/lib/urgency.test.ts new file mode 100644 index 0000000..6c07f6d --- /dev/null +++ b/src/lib/urgency.test.ts @@ -0,0 +1,373 @@ +import { describe, it, expect } from 'vitest'; +import { computeUrgencyScore, urgencyLevel } from './urgency'; + +// Helper to create a YYYY-MM-DD date string N days from now (UTC) +function daysFromNow(n: number): string { + const d = new Date(); + d.setUTCDate(d.getUTCDate() + n); + return d.toISOString().slice(0, 10); +} + +const base = { + category: 'utility', + status: 'pending', + auto_pay: false, + late_fee: null as number | null, + grace_period_days: 0, +}; + +describe('computeUrgencyScore', () => { + // ── Time pressure boundaries ──────────────────────────── + + describe('time pressure', () => { + it('scores 0 time pressure for due > 14 days out', () => { + const score = computeUrgencyScore({ ...base, due_date: daysFromNow(30) }); + // category weight only (utility = 15) + expect(score).toBe(15); + }); + + it('scores 10 for due in exactly 14 days', () => { + const score = computeUrgencyScore({ ...base, due_date: daysFromNow(14) }); + expect(score).toBe(10 + 15); // time + utility + }); + + it('scores 20 for due in exactly 7 days', () => { + const score = computeUrgencyScore({ ...base, due_date: daysFromNow(7) }); + expect(score).toBe(20 + 15); + }); + + it('scores 30 for due in exactly 3 days', () => { + const score = computeUrgencyScore({ ...base, due_date: daysFromNow(3) }); + expect(score).toBe(30 + 15); + }); + + it('scores 35 for due today', () => { + const score = computeUrgencyScore({ ...base, due_date: daysFromNow(0) }); + expect(score).toBe(35 + 15); + }); + + it('scores 40 for 1 day overdue', () => { + const score = computeUrgencyScore({ ...base, due_date: daysFromNow(-1) }); + expect(score).toBe(40 + 15); + }); + + it('scores 45 for 8 days overdue', () => { + const score = computeUrgencyScore({ ...base, due_date: daysFromNow(-8) }); + expect(score).toBe(45 + 15); + }); + + it('scores 50 for 31+ days overdue', () => { + const score = computeUrgencyScore({ ...base, due_date: daysFromNow(-31) }); + expect(score).toBe(50 + 15); + }); + + it('scores 10 for due in 8 days (between 7 and 14)', () => { + const score = computeUrgencyScore({ ...base, due_date: daysFromNow(8) }); + expect(score).toBe(10 + 15); + }); + + it('scores 20 for due in 4 days (between 3 and 7)', () => { + const score = computeUrgencyScore({ ...base, due_date: daysFromNow(4) }); + expect(score).toBe(20 + 15); + }); + + it('scores 30 for due in 1 day (between 0 and 3)', () => { + const score = computeUrgencyScore({ ...base, due_date: daysFromNow(1) }); + expect(score).toBe(30 + 15); + }); + }); + + // ── Category weights ──────────────────────────────────── + + describe('category weights', () => { + const futureDue = daysFromNow(30); // 0 time pressure + + it('legal = 30', () => { + expect(computeUrgencyScore({ ...base, due_date: futureDue, category: 'legal' })).toBe(30); + }); + + it('mortgage = 25', () => { + expect(computeUrgencyScore({ ...base, due_date: futureDue, category: 'mortgage' })).toBe(25); + }); + + it('property_tax = 20', () => { + expect(computeUrgencyScore({ ...base, due_date: futureDue, category: 'property_tax' })).toBe(20); + }); + + it('federal_tax = 20', () => { + expect(computeUrgencyScore({ ...base, due_date: futureDue, category: 'federal_tax' })).toBe(20); + }); + + it('utility = 15', () => { + expect(computeUrgencyScore({ ...base, due_date: futureDue, category: 'utility' })).toBe(15); + }); + + it('insurance = 15', () => { + expect(computeUrgencyScore({ ...base, due_date: futureDue, category: 'insurance' })).toBe(15); + }); + + it('hoa = 12', () => { + expect(computeUrgencyScore({ ...base, due_date: futureDue, category: 'hoa' })).toBe(12); + }); + + it('credit_card = 10', () => { + expect(computeUrgencyScore({ ...base, due_date: futureDue, category: 'credit_card' })).toBe(10); + }); + + it('loan = 10', () => { + expect(computeUrgencyScore({ ...base, due_date: futureDue, category: 'loan' })).toBe(10); + }); + + it('subscription = 5', () => { + expect(computeUrgencyScore({ ...base, due_date: futureDue, category: 'subscription' })).toBe(5); + }); + + it('unknown category defaults to 5', () => { + expect(computeUrgencyScore({ ...base, due_date: futureDue, category: 'random_thing' })).toBe(5); + }); + + it('empty string category defaults to 5', () => { + expect(computeUrgencyScore({ ...base, due_date: futureDue, category: '' })).toBe(5); + }); + }); + + // ── Late fee tiers ────────────────────────────────────── + + describe('late fees', () => { + const futureDue = daysFromNow(30); + + it('no late fee adds 0', () => { + expect(computeUrgencyScore({ ...base, due_date: futureDue, late_fee: null })).toBe(15); + }); + + it('late fee of 0 adds 0', () => { + expect(computeUrgencyScore({ ...base, due_date: futureDue, late_fee: 0 })).toBe(15); + }); + + it('late fee of $10 adds 5', () => { + expect(computeUrgencyScore({ ...base, due_date: futureDue, late_fee: 10 })).toBe(15 + 5); + }); + + it('late fee of $25 adds 5 (boundary, not > 25)', () => { + expect(computeUrgencyScore({ ...base, due_date: futureDue, late_fee: 25 })).toBe(15 + 5); + }); + + it('late fee of $26 adds 10', () => { + expect(computeUrgencyScore({ ...base, due_date: futureDue, late_fee: 26 })).toBe(15 + 10); + }); + + it('late fee of $50 adds 10 (boundary, not > 50)', () => { + expect(computeUrgencyScore({ ...base, due_date: futureDue, late_fee: 50 })).toBe(15 + 10); + }); + + it('late fee of $51 adds 15', () => { + expect(computeUrgencyScore({ ...base, due_date: futureDue, late_fee: 51 })).toBe(15 + 15); + }); + + it('late fee of $200 adds 15', () => { + expect(computeUrgencyScore({ ...base, due_date: futureDue, late_fee: 200 })).toBe(15 + 15); + }); + }); + + // ── Status modifiers ──────────────────────────────────── + + describe('status modifiers', () => { + const futureDue = daysFromNow(30); + + it('paid status reduces by 50 (clamped to 0)', () => { + const score = computeUrgencyScore({ ...base, due_date: futureDue, status: 'paid' }); + // 0 time + 15 category - 50 paid = -35 → clamped to 0 + expect(score).toBe(0); + }); + + it('disputed status reduces by 10', () => { + const score = computeUrgencyScore({ ...base, due_date: futureDue, status: 'disputed' }); + expect(score).toBe(15 - 10); // 5 + }); + + it('deferred status reduces by 15', () => { + const score = computeUrgencyScore({ ...base, due_date: futureDue, status: 'deferred' }); + expect(score).toBe(15 - 15); // 0 + }); + + it('overdue status has no additional modifier (time pressure handles it)', () => { + // Overdue is about the date, not a status modifier + const score = computeUrgencyScore({ ...base, due_date: futureDue, status: 'overdue' }); + expect(score).toBe(15); // just category weight + }); + }); + + // ── Auto-pay modifier ─────────────────────────────────── + + describe('auto_pay', () => { + const futureDue = daysFromNow(30); + + it('auto_pay reduces by 25', () => { + const score = computeUrgencyScore({ ...base, due_date: futureDue, auto_pay: true }); + // 0 time + 15 category - 25 auto_pay = -10 → clamped to 0 + expect(score).toBe(0); + }); + + it('auto_pay on a due-today obligation still lowers score', () => { + const score = computeUrgencyScore({ ...base, due_date: daysFromNow(0), auto_pay: true }); + // 35 time + 15 category - 25 auto_pay = 25 + expect(score).toBe(25); + }); + }); + + // ── Grace period ──────────────────────────────────────── + + describe('grace period', () => { + it('grace period shifts effective due date for time pressure', () => { + // Due 1 day ago, but 5 day grace period → effectively 4 days out + const score = computeUrgencyScore({ + ...base, + due_date: daysFromNow(-1), + grace_period_days: 5, + }); + // With grace: effective = 4 days out → 20 time pressure + 15 category = 35 + expect(score).toBe(20 + 15); + }); + + it('grace period of 0 has no effect', () => { + const score = computeUrgencyScore({ + ...base, + due_date: daysFromNow(-1), + grace_period_days: 0, + }); + // 1 day overdue → 40 time + 15 category = 55 + expect(score).toBe(40 + 15); + }); + + it('grace period does not reduce overdue severity when fully expired', () => { + // 10 days overdue, 3 day grace → effectively 7 days overdue + const score = computeUrgencyScore({ + ...base, + due_date: daysFromNow(-10), + grace_period_days: 3, + }); + // Effective = 7 days overdue → daysUntilDue = -7 → < -7 is false, < 0 is true → 40 + expect(score).toBe(40 + 15); + }); + }); + + // ── Combined modifiers ────────────────────────────────── + + describe('combined modifiers', () => { + it('auto_pay + paid stacks (both reduce)', () => { + const score = computeUrgencyScore({ + ...base, + due_date: daysFromNow(0), + auto_pay: true, + status: 'paid', + }); + // 35 time + 15 category - 25 auto_pay - 50 paid = -25 → clamped to 0 + expect(score).toBe(0); + }); + + it('auto_pay + disputed stacks', () => { + const score = computeUrgencyScore({ + ...base, + due_date: daysFromNow(0), + auto_pay: true, + status: 'disputed', + }); + // 35 + 15 - 25 - 10 = 15 + expect(score).toBe(15); + }); + + it('max possible score: severely overdue legal with high late fee', () => { + const score = computeUrgencyScore({ + ...base, + due_date: daysFromNow(-60), + category: 'legal', + late_fee: 100, + }); + // 50 time + 30 legal + 15 late fee = 95 + expect(score).toBe(95); + }); + + it('score is clamped to 100 even with theoretical overflow', () => { + // This tests the upper clamp — construct a high score + const score = computeUrgencyScore({ + ...base, + due_date: daysFromNow(-60), + category: 'legal', + late_fee: 100, + }); + expect(score).toBeLessThanOrEqual(100); + }); + + it('score is clamped to 0 even with heavy reductions', () => { + const score = computeUrgencyScore({ + ...base, + due_date: daysFromNow(30), + category: 'subscription', + auto_pay: true, + status: 'paid', + }); + // 0 + 5 - 25 - 50 = -70 → clamped to 0 + expect(score).toBe(0); + }); + }); + + // ── Invalid / edge case inputs ────────────────────────── + + describe('invalid inputs', () => { + it('invalid date string returns 0 (fails gracefully)', () => { + const score = computeUrgencyScore({ ...base, due_date: 'not-a-date' }); + // Should handle NaN gracefully, not crash + expect(score).toBeGreaterThanOrEqual(0); + expect(score).toBeLessThanOrEqual(100); + }); + + it('empty date string returns category weight only (no time pressure)', () => { + const score = computeUrgencyScore({ ...base, due_date: '' }); + expect(score).toBeGreaterThanOrEqual(0); + expect(score).toBeLessThanOrEqual(100); + }); + + it('NaN late_fee is treated as no late fee', () => { + const score = computeUrgencyScore({ ...base, due_date: daysFromNow(30), late_fee: NaN }); + expect(score).toBe(15); // just category weight + }); + + it('negative late_fee is treated as no late fee', () => { + const score = computeUrgencyScore({ ...base, due_date: daysFromNow(30), late_fee: -10 }); + expect(score).toBe(15); + }); + + it('negative grace_period_days does not break scoring', () => { + const score = computeUrgencyScore({ + ...base, + due_date: daysFromNow(3), + grace_period_days: -5, + }); + // Should not make things worse — treat as 0 + expect(score).toBeGreaterThanOrEqual(0); + }); + + it('NaN grace_period_days is treated as 0', () => { + const score = computeUrgencyScore({ + ...base, + due_date: daysFromNow(-1), + grace_period_days: NaN, + }); + // 1 day overdue (40 time + 15 category = 55) + expect(score).toBe(55); + }); + }); +}); + +// ── urgencyLevel ────────────────────────────────────────── + +describe('urgencyLevel', () => { + it('returns critical for 70', () => expect(urgencyLevel(70)).toBe('critical')); + it('returns critical for 100', () => expect(urgencyLevel(100)).toBe('critical')); + it('returns high for 50', () => expect(urgencyLevel(50)).toBe('high')); + it('returns high for 69', () => expect(urgencyLevel(69)).toBe('high')); + it('returns medium for 30', () => expect(urgencyLevel(30)).toBe('medium')); + it('returns medium for 49', () => expect(urgencyLevel(49)).toBe('medium')); + it('returns low for 29', () => expect(urgencyLevel(29)).toBe('low')); + it('returns low for 0', () => expect(urgencyLevel(0)).toBe('low')); +}); diff --git a/src/lib/urgency.ts b/src/lib/urgency.ts index 184695a..9710d14 100644 --- a/src/lib/urgency.ts +++ b/src/lib/urgency.ts @@ -11,18 +11,31 @@ export function computeUrgencyScore(obligation: { grace_period_days: number; }): number { let score = 0; + + // Parse dates as date-only (midnight UTC) to avoid time-of-day drift + // Cloudflare Workers run in UTC, so this is consistent across environments const now = new Date(); - const due = new Date(obligation.due_date); - const daysUntilDue = Math.floor((due.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); + const today = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); + const due = new Date(obligation.due_date + 'T00:00:00Z'); + + // Guard against invalid dates + if (isNaN(due.getTime())) { + // Can't compute time pressure — fall through to category weight only + } else { + // Apply grace period: shift effective due date forward + const graceDays = Math.max(0, obligation.grace_period_days || 0); + const effectiveDueMs = due.getTime() + graceDays * 86400000; + const daysUntilDue = Math.floor((effectiveDueMs - today.getTime()) / 86400000); - // Time pressure - if (daysUntilDue < -30) score += 50; // severely overdue - else if (daysUntilDue < -7) score += 45; // overdue > 1 week - else if (daysUntilDue < 0) score += 40; // overdue - else if (daysUntilDue === 0) score += 35; // due today - else if (daysUntilDue <= 3) score += 30; // due in 3 days - else if (daysUntilDue <= 7) score += 20; // due in a week - else if (daysUntilDue <= 14) score += 10; // due in 2 weeks + // Time pressure + if (daysUntilDue < -30) score += 50; // severely overdue + else if (daysUntilDue < -7) score += 45; // overdue > 1 week + else if (daysUntilDue < 0) score += 40; // overdue + else if (daysUntilDue === 0) score += 35; // due today + else if (daysUntilDue <= 3) score += 30; // due in 3 days + else if (daysUntilDue <= 7) score += 20; // due in a week + else if (daysUntilDue <= 14) score += 10; // due in 2 weeks + } // Consequence severity by category const categoryWeights: Record = { @@ -39,17 +52,19 @@ export function computeUrgencyScore(obligation: { }; score += categoryWeights[obligation.category] || 5; - // Late fee increases urgency - if (obligation.late_fee && obligation.late_fee > 50) score += 15; - else if (obligation.late_fee && obligation.late_fee > 25) score += 10; - else if (obligation.late_fee && obligation.late_fee > 0) score += 5; + // Late fee increases urgency (guard against NaN/negative) + const lateFee = obligation.late_fee != null && isFinite(obligation.late_fee) ? obligation.late_fee : 0; + if (lateFee > 50) score += 15; + else if (lateFee > 25) score += 10; + else if (lateFee > 0) score += 5; // Auto-pay reduces urgency (it's handled) if (obligation.auto_pay) score -= 25; - // Already paid or disputed reduces urgency + // Status modifiers if (obligation.status === 'paid') score -= 50; if (obligation.status === 'disputed') score -= 10; + if (obligation.status === 'deferred') score -= 15; return Math.min(100, Math.max(0, score)); } diff --git a/src/lib/validators.ts b/src/lib/validators.ts index e067bfc..5f7c2b8 100644 --- a/src/lib/validators.ts +++ b/src/lib/validators.ts @@ -112,6 +112,24 @@ export const actOnRecommendationSchema = z.object({ action_taken: z.string().min(1).max(1000).optional(), }); +// ── Cash Flow ──────────────────────────────────────────────── + +export const cashflowScenarioSchema = z.object({ + defer_obligation_ids: z.array(z.string().uuid()).min(1, 'At least one obligation ID required'), +}); + +// ── Auth ───────────────────────────────────────────────────── + +export const loginSchema = z.object({ + email: z.string().email(), + password: z.string().min(1), +}); + +export const registerSchema = z.object({ + email: z.string().email(), + password: z.string().min(8, 'Password must be at least 8 characters'), +}); + // ── Bridge: Ledger ─────────────────────────────────────────── export const recordActionSchema = z.object({ @@ -120,8 +138,52 @@ export const recordActionSchema = z.object({ notes: z.string().max(2000).optional(), }); +// ── Bridge: Books ──────────────────────────────────────────── + +export const recordBookTransactionSchema = z.object({ + type: z.enum(['income', 'expense']), + description: z.string().min(1).max(1000), + amount: z.number().positive(), +}); + +// ── Bridge: Assets ─────────────────────────────────────────── + +export const submitEvidenceSchema = z.object({ + evidenceType: z.string().min(1).max(255), + data: z.record(z.string(), z.unknown()), + metadata: z.record(z.string(), z.unknown()).optional(), +}); + +// ── Bridge: Scrape ─────────────────────────────────────────── + +export const courtDocketScrapeSchema = z.object({ + caseNumber: z.string().max(50).optional(), +}); + // ── Bridge: Plaid ──────────────────────────────────────────── export const exchangeTokenSchema = z.object({ public_token: z.string().min(1), }); + +// ── Query Param Schemas ────────────────────────────────────── + +const dateString = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Must be YYYY-MM-DD'); + +export const obligationQuerySchema = z.object({ + status: z.enum(['pending', 'overdue', 'paid', 'deferred', 'disputed']).optional(), + category: z.string().max(100).optional(), +}); + +export const obligationCalendarQuerySchema = z.object({ + start: dateString.optional(), + end: dateString.optional(), +}); + +export const disputeQuerySchema = z.object({ + status: z.string().max(50).optional(), +}); + +export const recommendationQuerySchema = z.object({ + status: z.string().max(50).optional(), +}); diff --git a/src/routes/auth.ts b/src/routes/auth.ts index f26e070..8bcf0cb 100644 --- a/src/routes/auth.ts +++ b/src/routes/auth.ts @@ -1,6 +1,7 @@ import { Hono } from 'hono'; import type { Env } from '../index'; import type { AuthVariables } from '../middleware/auth'; +import { loginSchema, registerSchema } from '../lib/validators'; /** * Auth routes — local auth with KV-stored credentials, falling back to ChittyAuth proxy. @@ -21,15 +22,10 @@ authRoutes.get('/login', (c) => { * Returns: { token: string, user_id: string, scopes: string[] } */ authRoutes.post('/login', async (c) => { - const body = await c.req.json().catch(() => null); - if (!body || typeof body !== 'object') { - return c.json({ error: 'Invalid request body' }, 400); - } - - const { email, password } = body as { email?: string; password?: string }; - if (!email || !password) { - return c.json({ error: 'Email and password are required' }, 400); - } + const raw = await c.req.json().catch(() => null); + const parsed = loginSchema.safeParse(raw); + if (!parsed.success) return c.json({ error: 'Validation failed', issues: parsed.error.issues }, 400); + const { email, password } = parsed.data; // ── Local auth: check KV-stored credentials ── const storedHash = await c.env.COMMAND_KV.get(`auth:user:${email.toLowerCase()}`); @@ -76,18 +72,10 @@ authRoutes.post('/login', async (c) => { * Body: { email: string, password: string } */ authRoutes.post('/register', async (c) => { - const body = await c.req.json().catch(() => null); - if (!body || typeof body !== 'object') { - return c.json({ error: 'Invalid request body' }, 400); - } - - const { email, password } = body as { email?: string; password?: string }; - if (!email || !password) { - return c.json({ error: 'Email and password are required' }, 400); - } - if (password.length < 8) { - return c.json({ error: 'Password must be at least 8 characters' }, 400); - } + const raw = await c.req.json().catch(() => null); + const parsed = registerSchema.safeParse(raw); + if (!parsed.success) return c.json({ error: 'Validation failed', issues: parsed.error.issues }, 400); + const { email, password } = parsed.data; const key = `auth:user:${email.toLowerCase()}`; const existing = await c.env.COMMAND_KV.get(key); diff --git a/src/routes/bridge.ts b/src/routes/bridge.ts index 13facd2..bf6e711 100644 --- a/src/routes/bridge.ts +++ b/src/routes/bridge.ts @@ -3,7 +3,7 @@ import type { Env } from '../index'; import type { AuthVariables } from '../middleware/auth'; import { getDb } from '../lib/db'; import { ledgerClient, financeClient, plaidClient, mercuryClient, connectClient, booksClient, assetsClient, scrapeClient } from '../lib/integrations'; -import { recordActionSchema, exchangeTokenSchema } from '../lib/validators'; +import { recordActionSchema, exchangeTokenSchema, recordBookTransactionSchema, submitEvidenceSchema, courtDocketScrapeSchema } from '../lib/validators'; export const bridgeRoutes = new Hono<{ Bindings: Env; Variables: AuthVariables }>(); @@ -545,10 +545,9 @@ bridgeRoutes.post('/books/record-transaction', async (c) => { const books = booksClient(c.env); if (!books) return c.json({ error: 'ChittyBooks not configured' }, 503); - const body = await c.req.json() as { type: 'income' | 'expense'; description: string; amount: number }; - if (!body.type || !body.description || !body.amount) { - return c.json({ error: 'type, description, and amount are required' }, 400); - } + const parsed = recordBookTransactionSchema.safeParse(await c.req.json()); + if (!parsed.success) return c.json({ error: 'Validation failed', issues: parsed.error.issues }, 400); + const body = parsed.data; const result = await books.recordTransaction(body); if (!result) return c.json({ error: 'Failed to record transaction in ChittyBooks' }, 502); @@ -621,10 +620,9 @@ bridgeRoutes.post('/assets/submit-evidence', async (c) => { const assets = assetsClient(c.env); if (!assets) return c.json({ error: 'ChittyAssets not configured' }, 503); - const body = await c.req.json() as { evidenceType: string; data: Record; metadata?: Record }; - if (!body.evidenceType || !body.data) { - return c.json({ error: 'evidenceType and data are required' }, 400); - } + const parsed = submitEvidenceSchema.safeParse(await c.req.json()); + if (!parsed.success) return c.json({ error: 'Validation failed', issues: parsed.error.issues }, 400); + const body = parsed.data; const result = await assets.submitEvidence({ evidenceType: body.evidenceType, @@ -653,8 +651,9 @@ bridgeRoutes.post('/scrape/court-docket', async (c) => { const token = await c.env.COMMAND_KV.get('scrape:service_token'); if (!token) return c.json({ error: 'Scrape service token not configured' }, 503); - const { caseNumber } = await c.req.json() as { caseNumber?: string }; - const targetCase = caseNumber || '2024D007847'; + const parsed = courtDocketScrapeSchema.safeParse(await c.req.json()); + if (!parsed.success) return c.json({ error: 'Validation failed', issues: parsed.error.issues }, 400); + const targetCase = parsed.data.caseNumber || '2024D007847'; const result = await scrape.scrapeCourtDocket(targetCase, token); diff --git a/src/routes/cashflow.ts b/src/routes/cashflow.ts index 4faf049..013b6b5 100644 --- a/src/routes/cashflow.ts +++ b/src/routes/cashflow.ts @@ -2,6 +2,7 @@ import { Hono } from 'hono'; import type { Env } from '../index'; import { getDb } from '../lib/db'; import { generateProjections } from '../lib/projections'; +import { cashflowScenarioSchema } from '../lib/validators'; export const cashflowRoutes = new Hono<{ Bindings: Env }>(); @@ -26,8 +27,9 @@ cashflowRoutes.post('/generate', async (c) => { // Scenario: "what if I defer obligation X?" cashflowRoutes.post('/scenario', async (c) => { - const { defer_obligation_ids } = await c.req.json() as { defer_obligation_ids: string[] }; - if (!defer_obligation_ids?.length) return c.json({ error: 'defer_obligation_ids required' }, 400); + const parsed = cashflowScenarioSchema.safeParse(await c.req.json()); + if (!parsed.success) return c.json({ error: 'Validation failed', issues: parsed.error.issues }, 400); + const { defer_obligation_ids } = parsed.data; const sql = getDb(c.env); diff --git a/src/routes/disputes.ts b/src/routes/disputes.ts index ab2093c..29bba0d 100644 --- a/src/routes/disputes.ts +++ b/src/routes/disputes.ts @@ -2,14 +2,16 @@ import { Hono } from 'hono'; import type { Env } from '../index'; import { getDb } from '../lib/db'; import { ledgerClient } from '../lib/integrations'; -import { createDisputeSchema, updateDisputeSchema, createCorrespondenceSchema } from '../lib/validators'; +import { createDisputeSchema, updateDisputeSchema, createCorrespondenceSchema, disputeQuerySchema } from '../lib/validators'; export const disputeRoutes = new Hono<{ Bindings: Env }>(); // List disputes disputeRoutes.get('/', async (c) => { const sql = getDb(c.env); - const status = c.req.query('status') || 'open'; + const qResult = disputeQuerySchema.safeParse({ status: c.req.query('status') }); + if (!qResult.success) return c.json({ error: 'Invalid query params', issues: qResult.error.issues }, 400); + const status = qResult.data.status || 'open'; const disputes = await sql` SELECT * FROM cc_disputes WHERE status = ${status} ORDER BY priority ASC, created_at DESC `; diff --git a/src/routes/mcp.ts b/src/routes/mcp.ts index fd5118c..06755c0 100644 --- a/src/routes/mcp.ts +++ b/src/routes/mcp.ts @@ -108,8 +108,8 @@ mcpRoutes.post('/', async (c) => { return c.json({ jsonrpc: '2.0', id, result: { tools: TOOLS } }); case 'tools/call': { - const toolName = params?.name; - const args = params?.arguments || {}; + const toolName = params?.name as string; + const args = (params?.arguments || {}) as Record; try { const sql = getDb(c.env); const result = await executeTool(sql, toolName, args); diff --git a/src/routes/obligations.ts b/src/routes/obligations.ts index e30cce8..50441ba 100644 --- a/src/routes/obligations.ts +++ b/src/routes/obligations.ts @@ -3,15 +3,17 @@ import type { Env } from '../index'; import { getDb } from '../lib/db'; import { computeUrgencyScore } from '../lib/urgency'; import { chargeClient } from '../lib/integrations'; -import { createObligationSchema, updateObligationSchema } from '../lib/validators'; +import { createObligationSchema, updateObligationSchema, obligationQuerySchema, obligationCalendarQuerySchema } from '../lib/validators'; export const obligationRoutes = new Hono<{ Bindings: Env }>(); // List obligations with filtering obligationRoutes.get('/', async (c) => { const sql = getDb(c.env); - const status = c.req.query('status'); - const category = c.req.query('category'); + const qResult = obligationQuerySchema.safeParse({ status: c.req.query('status'), category: c.req.query('category') }); + if (!qResult.success) return c.json({ error: 'Invalid query params', issues: qResult.error.issues }, 400); + const status = qResult.data.status || null; + const category = qResult.data.category || null; let obligations; if (status && category) { @@ -29,8 +31,10 @@ obligationRoutes.get('/', async (c) => { // Calendar view: obligations grouped by due date obligationRoutes.get('/calendar', async (c) => { const sql = getDb(c.env); - const start = c.req.query('start') || new Date().toISOString().slice(0, 10); - const end = c.req.query('end') || new Date(Date.now() + 30 * 86400000).toISOString().slice(0, 10); + const calResult = obligationCalendarQuerySchema.safeParse({ start: c.req.query('start'), end: c.req.query('end') }); + if (!calResult.success) return c.json({ error: 'Invalid query params', issues: calResult.error.issues }, 400); + const start = calResult.data.start || new Date().toISOString().slice(0, 10); + const end = calResult.data.end || new Date(Date.now() + 30 * 86400000).toISOString().slice(0, 10); const obligations = await sql` SELECT id, payee, category, amount_due, due_date, status, urgency_score, auto_pay @@ -150,11 +154,13 @@ obligationRoutes.post('/recalculate-urgency', async (c) => { category: ob.category, status: ob.status, auto_pay: ob.auto_pay, - late_fee: ob.late_fee ? parseFloat(ob.late_fee) : null, + late_fee: ob.late_fee ? (isFinite(parseFloat(ob.late_fee)) ? parseFloat(ob.late_fee) : null) : null, grace_period_days: ob.grace_period_days || 0, }); - const dueDate = new Date(ob.due_date); - const newStatus = dueDate < new Date() && ob.status === 'pending' ? 'overdue' : ob.status; + const dueDate = new Date(ob.due_date + 'T00:00:00Z'); + const now = new Date(); + const todayUtc = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); + const newStatus = dueDate < todayUtc && ob.status === 'pending' ? 'overdue' : ob.status; updates.push({ id: ob.id, score, status: newStatus }); } diff --git a/src/routes/recommendations.ts b/src/routes/recommendations.ts index ae3ef0a..bd49392 100644 --- a/src/routes/recommendations.ts +++ b/src/routes/recommendations.ts @@ -1,14 +1,16 @@ import { Hono } from 'hono'; import type { Env } from '../index'; import { getDb } from '../lib/db'; -import { actOnRecommendationSchema } from '../lib/validators'; +import { actOnRecommendationSchema, recommendationQuerySchema } from '../lib/validators'; import { runTriage } from '../lib/triage'; export const recommendationRoutes = new Hono<{ Bindings: Env }>(); recommendationRoutes.get('/', async (c) => { const sql = getDb(c.env); - const status = c.req.query('status') || 'active'; + const qResult = recommendationQuerySchema.safeParse({ status: c.req.query('status') }); + if (!qResult.success) return c.json({ error: 'Invalid query params', issues: qResult.error.issues }, 400); + const status = qResult.data.status || 'active'; const recs = await sql` SELECT r.*, o.payee as obligation_payee, o.amount_due, o.due_date, d.title as dispute_title, d.counterparty diff --git a/ui/index.html b/ui/index.html index 07782e4..4035aac 100644 --- a/ui/index.html +++ b/ui/index.html @@ -4,6 +4,9 @@ ChittyCommand + + +
diff --git a/ui/package-lock.json b/ui/package-lock.json index 0e705f6..e2eeeb0 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -12,12 +12,15 @@ "lucide-react": "^0.400.0", "react": "^18.3.0", "react-dom": "^18.3.0", + "react-grid-layout": "^2.2.2", "react-router-dom": "^6.23.0", + "recharts": "^3.7.0", "tailwind-merge": "^2.3.0" }, "devDependencies": { "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", + "@types/react-grid-layout": "^1.3.6", "@vitejs/plugin-react": "^4.3.0", "autoprefixer": "^10.4.0", "postcss": "^8.4.0", @@ -800,6 +803,42 @@ "node": ">= 8" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@remix-run/router": { "version": "1.23.2", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", @@ -1166,6 +1205,18 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1211,6 +1262,69 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1222,14 +1336,14 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.28", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -1246,6 +1360,22 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/react-grid-layout": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@types/react-grid-layout/-/react-grid-layout-1.3.6.tgz", + "integrity": "sha512-Cw7+sb3yyjtmxwwJiXtEXcu5h4cgs+sCGkHwHXsFmPyV30bf14LeD/fa2LwQovuD2HWxCcjIdNhDlcYGj95qGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1514,9 +1644,130 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1535,6 +1786,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -1556,6 +1813,16 @@ "dev": true, "license": "ISC" }, + "node_modules/es-toolkit": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", + "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -1605,6 +1872,18 @@ "node": ">=6" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", + "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==", + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -1733,6 +2012,25 @@ "node": ">= 0.4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1971,7 +2269,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -2197,6 +2494,23 @@ "dev": true, "license": "MIT" }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2243,6 +2557,68 @@ "react": "^18.3.1" } }, + "node_modules/react-draggable": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz", + "integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-grid-layout": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-2.2.2.tgz", + "integrity": "sha512-yNo9pxQWoxHWRAwHGSVT4DEGELYPyQ7+q9lFclb5jcqeFzva63/2F72CryS/jiTIr/SBIlTaDdyjqH+ODg8oBw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "fast-equals": "^4.0.3", + "prop-types": "^15.8.1", + "react-draggable": "^4.4.6", + "react-resizable": "^3.0.5", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -2253,6 +2629,20 @@ "node": ">=0.10.0" } }, + "node_modules/react-resizable": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.1.3.tgz", + "integrity": "sha512-liJBNayhX7qA4tBJiBD321FDhJxgGTJ07uzH5zSORXoE8h7PyEZ8mLqmosST7ppf6C4zUsbd2gzDMmBCfFp9Lw==", + "license": "MIT", + "dependencies": { + "prop-types": "15.x", + "react-draggable": "^4.5.0" + }, + "peerDependencies": { + "react": ">= 16.3", + "react-dom": ">= 16.3" + } + }, "node_modules/react-router": { "version": "6.30.3", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", @@ -2308,6 +2698,63 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz", + "integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -2545,6 +2992,12 @@ "node": ">=0.8" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -2658,6 +3111,15 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2665,6 +3127,28 @@ "dev": true, "license": "MIT" }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", diff --git a/ui/package.json b/ui/package.json index 460a687..0d8c553 100644 --- a/ui/package.json +++ b/ui/package.json @@ -9,16 +9,19 @@ "preview": "vite preview" }, "dependencies": { + "clsx": "^2.1.0", + "lucide-react": "^0.400.0", "react": "^18.3.0", "react-dom": "^18.3.0", + "react-grid-layout": "^2.2.2", "react-router-dom": "^6.23.0", - "lucide-react": "^0.400.0", - "clsx": "^2.1.0", + "recharts": "^3.7.0", "tailwind-merge": "^2.3.0" }, "devDependencies": { "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", + "@types/react-grid-layout": "^1.3.6", "@vitejs/plugin-react": "^4.3.0", "autoprefixer": "^10.4.0", "postcss": "^8.4.0", diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index 9275847..0401a9b 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -1,66 +1,17 @@ -import { Outlet, NavLink } from 'react-router-dom'; -import { cn } from '../lib/utils'; -import { logout, getUser } from '../lib/auth'; - -const navItems = [ - { path: '/', label: 'Dashboard' }, - { path: '/bills', label: 'Bills' }, - { path: '/disputes', label: 'Disputes' }, - { path: '/accounts', label: 'Accounts' }, - { path: '/legal', label: 'Legal' }, - { path: '/recommendations', label: 'AI Recs' }, - { path: '/cashflow', label: 'Cash Flow' }, - { path: '/upload', label: 'Upload' }, - { path: '/settings', label: 'Settings' }, -]; +import { Outlet } from 'react-router-dom'; +import { Sidebar } from './Sidebar'; +import { StatusBar } from './StatusBar'; export function Layout() { - const user = getUser(); - return ( -
-
-
-

- ChittyCommand -

-
- -
- {user && ( - {user.user_id} - )} - -
-
-
-
-
- -
+
+ +
+ +
+ +
+
); } diff --git a/ui/src/components/Sidebar.tsx b/ui/src/components/Sidebar.tsx new file mode 100644 index 0000000..daca729 --- /dev/null +++ b/ui/src/components/Sidebar.tsx @@ -0,0 +1,65 @@ +import { NavLink } from 'react-router-dom'; +import { cn } from '../lib/utils'; +import { + LayoutDashboard, Receipt, ShieldAlert, Wallet, Scale, + Lightbulb, TrendingUp, Upload, Settings, LogOut, +} from 'lucide-react'; +import { logout, getUser } from '../lib/auth'; + +const navItems = [ + { path: '/', label: 'Dashboard', icon: LayoutDashboard }, + { path: '/bills', label: 'Bills', icon: Receipt }, + { path: '/disputes', label: 'Disputes', icon: ShieldAlert }, + { path: '/accounts', label: 'Accounts', icon: Wallet }, + { path: '/legal', label: 'Legal', icon: Scale }, + { path: '/recommendations', label: 'AI Recs', icon: Lightbulb }, + { path: '/cashflow', label: 'Cash Flow', icon: TrendingUp }, + { path: '/upload', label: 'Upload', icon: Upload }, + { path: '/settings', label: 'Settings', icon: Settings }, +]; + +export function Sidebar() { + const user = getUser(); + + return ( + + ); +} diff --git a/ui/src/components/StatusBar.tsx b/ui/src/components/StatusBar.tsx new file mode 100644 index 0000000..75399b3 --- /dev/null +++ b/ui/src/components/StatusBar.tsx @@ -0,0 +1,76 @@ +import { useEffect, useState } from 'react'; +import { useFocusMode } from '../lib/focus-mode'; +import { api, type DashboardData, type SyncStatus } from '../lib/api'; +import { FreshnessDot, freshnessFromDate } from './ui/FreshnessDot'; +import { Eye, EyeOff } from 'lucide-react'; + +export function StatusBar() { + const { focusMode, toggleFocusMode } = useFocusMode(); + const [data, setData] = useState(null); + const [syncs, setSyncs] = useState([]); + + useEffect(() => { + api.getDashboard().then(setData).catch((e) => console.error('[StatusBar] dashboard load failed:', e)); + api.getSyncStatus().then(setSyncs).catch((e) => console.error('[StatusBar] sync status load failed:', e)); + }, []); + + const cashPosition = data?.summary?.total_cash; + const overdueCount = data?.obligations?.overdue_count; + const dueThisWeek = data?.obligations?.due_this_week; + + // Pick the most recent sync per source for freshness display + const sourceFreshness = syncs.reduce>((acc, s) => { + const current = acc[s.source]; + if (!current || (s.completed_at && s.completed_at > current)) { + acc[s.source] = s.completed_at; + } + return acc; + }, {}); + + return ( +
+
+ {cashPosition && ( +
+ Cash + + ${Number(cashPosition).toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} + +
+ )} + {overdueCount && Number(overdueCount) > 0 && ( +
+ Overdue + {overdueCount} +
+ )} + {dueThisWeek && Number(dueThisWeek) > 0 && ( +
+ Due This Week + {dueThisWeek} +
+ )} + + {/* Freshness dots */} + {Object.keys(sourceFreshness).length > 0 && ( +
+ {Object.entries(sourceFreshness).map(([source, lastSync]) => ( + + + + ))} +
+ )} +
+ + +
+ ); +} diff --git a/ui/src/components/dashboard/DeadlinesWidget.tsx b/ui/src/components/dashboard/DeadlinesWidget.tsx new file mode 100644 index 0000000..89752ec --- /dev/null +++ b/ui/src/components/dashboard/DeadlinesWidget.tsx @@ -0,0 +1,37 @@ +import { Card } from '../ui/Card'; +import { urgencyFromDays } from '../ui/UrgencyBorder'; +import type { LegalDeadline } from '../../lib/api'; +import { formatDate, daysUntil } from '../../lib/utils'; + +interface Props { + deadlines: LegalDeadline[]; +} + +export function DeadlinesWidget({ deadlines }: Props) { + if (deadlines.length === 0) return null; + + return ( +
+

Legal Deadlines

+ {deadlines.map((dl) => { + const days = daysUntil(dl.deadline_date); + return ( + +
+
+

{dl.title}

+

{dl.case_ref}

+
+
+

+ {days > 0 ? `${days}d` : days === 0 ? 'TODAY' : `${Math.abs(days)}d ago`} +

+

{formatDate(dl.deadline_date)}

+
+
+
+ ); + })} +
+ ); +} diff --git a/ui/src/components/dashboard/DisputesWidget.tsx b/ui/src/components/dashboard/DisputesWidget.tsx new file mode 100644 index 0000000..2820cea --- /dev/null +++ b/ui/src/components/dashboard/DisputesWidget.tsx @@ -0,0 +1,51 @@ +import { Card } from '../ui/Card'; +import { ProgressDots } from '../ui/ProgressDots'; +import type { Dispute } from '../../lib/api'; +import { formatCurrency } from '../../lib/utils'; + +const DISPUTE_STAGES = ['filed', 'response_pending', 'in_review', 'resolved']; + +function disputeStageIndex(status: string): number { + const idx = DISPUTE_STAGES.indexOf(status); + return idx >= 0 ? idx : 0; +} + +interface Props { + disputes: Dispute[]; +} + +export function DisputesWidget({ disputes }: Props) { + if (disputes.length === 0) { + return ( +
+

Active Disputes

+

No active disputes

+
+ ); + } + + return ( +
+

Active Disputes

+ {disputes.map((d) => ( + +
+
+

{d.title}

+

vs {d.counterparty}

+ +
+
+ {d.amount_at_stake && ( +

{formatCurrency(d.amount_at_stake)}

+ )} + {d.next_action && ( + {d.next_action} + )} +
+
+
+ ))} +
+ ); +} diff --git a/ui/src/components/dashboard/FocusView.tsx b/ui/src/components/dashboard/FocusView.tsx new file mode 100644 index 0000000..1e63de1 --- /dev/null +++ b/ui/src/components/dashboard/FocusView.tsx @@ -0,0 +1,137 @@ +import { useNavigate } from 'react-router-dom'; +import { Card } from '../ui/Card'; +import { ActionButton } from '../ui/ActionButton'; +import { urgencyLevel } from '../ui/UrgencyBorder'; +import type { DashboardData, Obligation, Recommendation } from '../../lib/api'; +import { formatCurrency, formatDate, daysUntil } from '../../lib/utils'; + +interface FocusViewProps { + data: DashboardData; + onPayNow: (ob: Obligation) => void; + onExecute: (rec: Recommendation) => void; + payingId: string | null; + executingId: string | null; +} + +interface FocusItem { + id: string; + type: 'obligation' | 'dispute' | 'deadline' | 'recommendation'; + urgency: number; + title: string; + subtitle: string; + metric: string; + action: { label: string; onClick: () => void; loading: boolean }; +} + +export function FocusView({ data, onPayNow, onExecute, payingId, executingId }: FocusViewProps) { + const navigate = useNavigate(); + const { obligations, disputes, deadlines, recommendations } = data; + + const items: FocusItem[] = []; + + obligations.urgent.forEach((ob) => { + items.push({ + id: `ob-${ob.id}`, + type: 'obligation', + urgency: ob.urgency_score ?? 0, + title: ob.payee, + subtitle: ob.status === 'overdue' + ? `OVERDUE ${Math.abs(daysUntil(ob.due_date))} days` + : `Due ${formatDate(ob.due_date)}`, + metric: formatCurrency(ob.amount_due), + action: { + label: 'Pay Now', + onClick: () => onPayNow(ob), + loading: payingId === ob.id, + }, + }); + }); + + disputes.forEach((d) => { + items.push({ + id: `disp-${d.id}`, + type: 'dispute', + urgency: (6 - d.priority) * 20, + title: d.title, + subtitle: `vs ${d.counterparty}`, + metric: d.amount_at_stake ? formatCurrency(d.amount_at_stake) : '', + action: { + label: d.next_action ? 'Take Action' : 'View', + onClick: () => navigate('/disputes'), + loading: false, + }, + }); + }); + + deadlines.forEach((dl) => { + const days = daysUntil(dl.deadline_date); + items.push({ + id: `dl-${dl.id}`, + type: 'deadline', + urgency: dl.urgency_score ?? (days <= 7 ? 80 : 30), + title: dl.title, + subtitle: dl.case_ref, + metric: days > 0 ? `${days}d left` : days === 0 ? 'TODAY' : `${Math.abs(days)}d ago`, + action: { + label: 'View', + onClick: () => navigate('/legal'), + loading: false, + }, + }); + }); + + recommendations.slice(0, 3).forEach((rec) => { + items.push({ + id: `rec-${rec.id}`, + type: 'recommendation', + urgency: (6 - rec.priority) * 15, + title: rec.title, + subtitle: rec.reasoning, + metric: '', + action: { + label: rec.action_type ? 'Execute' : 'View', + onClick: () => rec.action_type ? onExecute(rec) : navigate('/recommendations'), + loading: executingId === rec.id, + }, + }); + }); + + const top3 = items.sort((a, b) => b.urgency - a.urgency).slice(0, 3); + + if (top3.length === 0) { + return ( +
+
+

All clear

+

Nothing urgent right now.

+
+
+ ); + } + + return ( +
+

Needs your attention

+ {top3.map((item) => ( + +
+

{item.title}

+

{item.subtitle}

+
+ {item.metric && ( +

{item.metric}

+ )} + +
+ ))} +
+ ); +} diff --git a/ui/src/components/dashboard/FullView.tsx b/ui/src/components/dashboard/FullView.tsx new file mode 100644 index 0000000..2791d39 --- /dev/null +++ b/ui/src/components/dashboard/FullView.tsx @@ -0,0 +1,44 @@ +import { MetricCard } from '../ui/MetricCard'; +import { ObligationsWidget } from './ObligationsWidget'; +import { DisputesWidget } from './DisputesWidget'; +import { DeadlinesWidget } from './DeadlinesWidget'; +import { RecommendationsWidget } from './RecommendationsWidget'; +import type { DashboardData, Obligation, Recommendation } from '../../lib/api'; +import { formatCurrency } from '../../lib/utils'; + +interface FullViewProps { + data: DashboardData; + onPayNow: (ob: Obligation) => void; + onExecute: (rec: Recommendation) => void; + payingId: string | null; + executingId: string | null; +} + +export function FullView({ data, onPayNow, onExecute, payingId, executingId }: FullViewProps) { + const { summary, obligations, disputes, deadlines, recommendations } = data; + + return ( +
+
+ + + + 0 ? 'text-urgency-red' : 'text-urgency-green'} + /> +
+ +
+ + +
+ +
+ + +
+
+ ); +} diff --git a/ui/src/components/dashboard/ObligationsWidget.tsx b/ui/src/components/dashboard/ObligationsWidget.tsx new file mode 100644 index 0000000..bacf3a1 --- /dev/null +++ b/ui/src/components/dashboard/ObligationsWidget.tsx @@ -0,0 +1,56 @@ +import { Card } from '../ui/Card'; +import { urgencyLevel } from '../ui/UrgencyBorder'; +import type { Obligation } from '../../lib/api'; +import { formatCurrency, formatDate, daysUntil } from '../../lib/utils'; + +interface Props { + obligations: Obligation[]; + onPayNow: (ob: Obligation) => void; + payingId: string | null; +} + +export function ObligationsWidget({ obligations, onPayNow, payingId }: Props) { + if (obligations.length === 0) { + return ( +
+

Upcoming Bills

+

No pending obligations

+
+ ); + } + + return ( +
+

Upcoming Bills

+ {obligations.map((ob) => { + const days = daysUntil(ob.due_date); + return ( + +
+
+

{ob.payee}

+

+ {ob.category} — {ob.status === 'overdue' + ? `${Math.abs(days)}d overdue` + : `Due ${formatDate(ob.due_date)}`} +

+
+
+

{formatCurrency(ob.amount_due)}

+ {ob.status !== 'paid' && ( + + )} +
+
+
+ ); + })} +
+ ); +} diff --git a/ui/src/components/dashboard/RecommendationsWidget.tsx b/ui/src/components/dashboard/RecommendationsWidget.tsx new file mode 100644 index 0000000..e023b4e --- /dev/null +++ b/ui/src/components/dashboard/RecommendationsWidget.tsx @@ -0,0 +1,40 @@ +import { Card } from '../ui/Card'; +import { ActionButton } from '../ui/ActionButton'; +import type { Recommendation } from '../../lib/api'; + +interface Props { + recommendations: Recommendation[]; + onExecute: (rec: Recommendation) => void; + executingId: string | null; +} + +export function RecommendationsWidget({ recommendations, onExecute, executingId }: Props) { + if (recommendations.length === 0) return null; + + return ( +
+

AI Recommendations

+ {recommendations.map((rec) => ( + +
+
+
+ {rec.rec_type} +
+

{rec.title}

+

{rec.reasoning}

+
+ {rec.action_type && ( + onExecute(rec)} + loading={executingId === rec.id} + className="shrink-0" + /> + )} +
+
+ ))} +
+ ); +} diff --git a/ui/src/components/ui/ActionButton.tsx b/ui/src/components/ui/ActionButton.tsx new file mode 100644 index 0000000..f188257 --- /dev/null +++ b/ui/src/components/ui/ActionButton.tsx @@ -0,0 +1,29 @@ +import { cn } from '../../lib/utils'; + +interface ActionButtonProps { + label: string; + onClick: () => void; + variant?: 'primary' | 'secondary' | 'danger'; + loading?: boolean; + disabled?: boolean; + className?: string; +} + +export function ActionButton({ label, onClick, variant = 'primary', loading, disabled, className }: ActionButtonProps) { + const base = 'px-4 py-2 text-sm font-medium rounded-lg transition-colors disabled:opacity-50'; + const variants = { + primary: 'bg-chitty-600 text-white hover:bg-chitty-700', + secondary: 'bg-card-border text-card-text hover:bg-gray-200', + danger: 'bg-urgency-red text-white hover:bg-red-600', + }; + + return ( + + ); +} diff --git a/ui/src/components/ui/Card.tsx b/ui/src/components/ui/Card.tsx new file mode 100644 index 0000000..02276ff --- /dev/null +++ b/ui/src/components/ui/Card.tsx @@ -0,0 +1,34 @@ +import { cn } from '../../lib/utils'; + +interface CardProps { + children: React.ReactNode; + className?: string; + urgency?: 'red' | 'amber' | 'green' | null; + muted?: boolean; + onClick?: () => void; +} + +export function Card({ children, className, urgency, muted, onClick }: CardProps) { + const borderColor = urgency === 'red' + ? 'border-l-urgency-red' + : urgency === 'amber' + ? 'border-l-urgency-amber' + : urgency === 'green' + ? 'border-l-urgency-green' + : 'border-l-transparent'; + + return ( +
+ {children} +
+ ); +} diff --git a/ui/src/components/ui/FreshnessDot.tsx b/ui/src/components/ui/FreshnessDot.tsx new file mode 100644 index 0000000..7571437 --- /dev/null +++ b/ui/src/components/ui/FreshnessDot.tsx @@ -0,0 +1,26 @@ +import { cn } from '../../lib/utils'; + +interface FreshnessDotProps { + status: 'fresh' | 'stale' | 'failed' | 'unknown'; + className?: string; +} + +export function FreshnessDot({ status, className }: FreshnessDotProps) { + const color = status === 'fresh' + ? 'bg-urgency-green' + : status === 'stale' + ? 'bg-urgency-amber' + : status === 'failed' + ? 'bg-urgency-red' + : 'bg-chrome-muted'; + + return ; +} + +export function freshnessFromDate(dateStr: string | null): 'fresh' | 'stale' | 'failed' | 'unknown' { + if (!dateStr) return 'unknown'; + const hours = (Date.now() - new Date(dateStr).getTime()) / (1000 * 60 * 60); + if (hours < 24) return 'fresh'; + if (hours < 72) return 'stale'; + return 'failed'; +} diff --git a/ui/src/components/ui/MetricCard.tsx b/ui/src/components/ui/MetricCard.tsx new file mode 100644 index 0000000..018434d --- /dev/null +++ b/ui/src/components/ui/MetricCard.tsx @@ -0,0 +1,22 @@ +import { cn } from '../../lib/utils'; + +interface MetricCardProps { + label: string; + value: string; + trend?: 'up' | 'down' | null; + className?: string; + valueClassName?: string; +} + +export function MetricCard({ label, value, trend, className, valueClassName }: MetricCardProps) { + return ( +
+

{label}

+
+

{value}

+ {trend === 'up' && } + {trend === 'down' && } +
+
+ ); +} diff --git a/ui/src/components/ui/ProgressDots.tsx b/ui/src/components/ui/ProgressDots.tsx new file mode 100644 index 0000000..1be60e1 --- /dev/null +++ b/ui/src/components/ui/ProgressDots.tsx @@ -0,0 +1,29 @@ +import { cn } from '../../lib/utils'; + +interface ProgressDotsProps { + completed: number; + total: number; + className?: string; +} + +export function ProgressDots({ completed, total, className }: ProgressDotsProps) { + const safeTotal = Math.max(1, Math.floor(total)); + const safeCompleted = Math.max(0, Math.min(Math.floor(completed), safeTotal)); + + return ( +
+ {Array.from({ length: safeTotal }, (_, i) => ( + + ))} + + {safeCompleted}/{safeTotal} + +
+ ); +} diff --git a/ui/src/components/ui/UrgencyBorder.ts b/ui/src/components/ui/UrgencyBorder.ts new file mode 100644 index 0000000..92acded --- /dev/null +++ b/ui/src/components/ui/UrgencyBorder.ts @@ -0,0 +1,12 @@ +export function urgencyLevel(score: number | null): 'red' | 'amber' | 'green' | null { + if (score === null || score === undefined) return null; + if (score >= 70) return 'red'; + if (score >= 40) return 'amber'; + return 'green'; +} + +export function urgencyFromDays(days: number): 'red' | 'amber' | 'green' { + if (days <= 2) return 'red'; + if (days <= 7) return 'amber'; + return 'green'; +} diff --git a/ui/src/index.css b/ui/src/index.css index 997cf72..54e4f18 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -2,9 +2,51 @@ @tailwind components; @tailwind utilities; +:root { + /* Chrome (dark shell) */ + --chrome-bg: #1a1a2e; + --chrome-surface: #16213e; + --chrome-border: #2a2a4a; + --chrome-text: #e2e8f0; + --chrome-text-muted: #94a3b8; + + /* Cards (light surfaces) */ + --card-bg: #ffffff; + --card-bg-hover: #f8fafc; + --card-border: #e2e8f0; + --card-text: #1e293b; + --card-text-muted: #64748b; + + /* Urgency */ + --urgency-red: #ef4444; + --urgency-amber: #f59e0b; + --urgency-green: #22c55e; + --urgency-red-bg: #fef2f2; + --urgency-amber-bg: #fffbeb; + --urgency-green-bg: #f0fdf4; + + /* Brand */ + --chitty-500: #4c6ef5; + --chitty-600: #3b5bdb; + --chitty-700: #364fc7; +} + body { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; - background-color: #0f1117; - color: #e4e5e7; + font-family: 'Outfit', -apple-system, BlinkMacSystemFont, sans-serif; + background-color: var(--chrome-bg); + color: var(--chrome-text); + -webkit-font-smoothing: antialiased; +} + +/* Monospace for financial numbers */ +.font-mono { + font-family: 'JetBrains Mono', ui-monospace, monospace; +} + +/* react-grid-layout overrides */ +.react-grid-item.react-grid-placeholder { + background: var(--chitty-500) !important; + opacity: 0.15 !important; + border-radius: 12px !important; } diff --git a/ui/src/lib/focus-mode.tsx b/ui/src/lib/focus-mode.tsx new file mode 100644 index 0000000..d7477e9 --- /dev/null +++ b/ui/src/lib/focus-mode.tsx @@ -0,0 +1,36 @@ +import { createContext, useContext, useState, useCallback, type ReactNode } from 'react'; + +interface FocusModeContextType { + focusMode: boolean; + toggleFocusMode: () => void; +} + +const FocusModeContext = createContext({ + focusMode: true, + toggleFocusMode: () => {}, +}); + +export function FocusModeProvider({ children }: { children: ReactNode }) { + const [focusMode, setFocusMode] = useState(() => { + const saved = localStorage.getItem('chittycommand_focus_mode'); + return saved !== null ? saved === 'true' : true; // default ON + }); + + const toggleFocusMode = useCallback(() => { + setFocusMode((prev) => { + const next = !prev; + localStorage.setItem('chittycommand_focus_mode', String(next)); + return next; + }); + }, []); + + return ( + + {children} + + ); +} + +export function useFocusMode() { + return useContext(FocusModeContext); +} diff --git a/ui/src/main.tsx b/ui/src/main.tsx index e9f8719..e5560b0 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -13,6 +13,7 @@ import { Recommendations } from './pages/Recommendations'; import { Settings } from './pages/Settings'; import { Login } from './pages/Login'; import { isAuthenticated } from './lib/auth'; +import { FocusModeProvider } from './lib/focus-mode'; import './index.css'; function ProtectedRoute({ children }: { children: React.ReactNode }) { @@ -24,21 +25,23 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) { ReactDOM.createRoot(document.getElementById('root')!).render( - - - } /> - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - + + + + } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + , ); diff --git a/ui/src/pages/Accounts.tsx b/ui/src/pages/Accounts.tsx index 71ce83f..dfeb52e 100644 --- a/ui/src/pages/Accounts.tsx +++ b/ui/src/pages/Accounts.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; import { api, type Account } from '../lib/api'; +import { Card } from '../components/ui/Card'; import { formatCurrency } from '../lib/utils'; export function Accounts() { @@ -10,7 +11,7 @@ export function Accounts() { api.getAccounts().then(setAccounts).catch((e) => setError(e.message)); }, []); - if (error) return

{error}

; + if (error) return

{error}

; const grouped = accounts.reduce>((acc, a) => { const type = a.account_type; @@ -28,52 +29,50 @@ export function Accounts() { loan: 'Loans', }; + const isDebtType = (type: string) => ['credit_card', 'store_credit', 'mortgage', 'loan'].includes(type); + return (
-

Accounts

+

Accounts

{Object.entries(grouped).map(([type, accts]) => (
-

+

{typeLabels[type] || type}

{accts.map((a) => ( -
+
-
-

{a.account_name}

-

{a.institution}

+
+

{a.account_name}

+

{a.institution}

-

+

{formatCurrency(a.current_balance)}

{a.credit_limit && ( -
-
+
+
-

+

{formatCurrency(a.current_balance)} / {formatCurrency(a.credit_limit)}

)} -
+ ))}
))} {accounts.length === 0 && ( -

No accounts configured

+

No accounts configured

)}
); diff --git a/ui/src/pages/Bills.tsx b/ui/src/pages/Bills.tsx index 7d3105e..7d2fc60 100644 --- a/ui/src/pages/Bills.tsx +++ b/ui/src/pages/Bills.tsx @@ -1,11 +1,15 @@ import { useEffect, useState } from 'react'; import { api, type Obligation } from '../lib/api'; -import { formatCurrency, formatDate, daysUntil, urgencyColor, statusBadgeColor } from '../lib/utils'; +import { Card } from '../components/ui/Card'; +import { ActionButton } from '../components/ui/ActionButton'; +import { urgencyLevel } from '../components/ui/UrgencyBorder'; +import { formatCurrency, formatDate, daysUntil, cn } from '../lib/utils'; export function Bills() { const [obligations, setObligations] = useState([]); const [filter, setFilter] = useState(''); const [error, setError] = useState(null); + const [payingId, setPayingId] = useState(null); useEffect(() => { const params: Record = {}; @@ -14,22 +18,38 @@ export function Bills() { }, [filter]); const handleMarkPaid = async (id: string) => { - await api.markPaid(id); - setObligations((prev) => prev.map((o) => (o.id === id ? { ...o, status: 'paid', urgency_score: 0 } : o))); + setPayingId(id); + try { + await api.markPaid(id); + setObligations((prev) => prev.map((o) => (o.id === id ? { ...o, status: 'paid', urgency_score: 0 } : o))); + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : 'Failed to mark as paid'; + console.error(`[Bills] markPaid failed for ${id}:`, msg, e); + setError(msg); + } finally { + setPayingId(null); + } }; - if (error) return

{error}

; + if (error) return

{error}

; + + const filters = ['', 'pending', 'overdue', 'paid']; return (
-

Bills & Obligations

-
- {['', 'pending', 'overdue', 'paid'].map((f) => ( +

Bills & Obligations

+
+ {filters.map((f) => ( @@ -37,64 +57,59 @@ export function Bills() {
-
- - - - - - - - - - - - - - {obligations.map((ob) => { - const days = ob.due_date ? daysUntil(ob.due_date) : null; - return ( - - - - - - - - - - ); - })} - -
PayeeCategoryAmountDue DateStatusUrgencyActions
{ob.payee}{ob.category}{formatCurrency(ob.amount_due)} - {ob.due_date ? ( - - {formatDate(ob.due_date)} - {days !== null && ({days > 0 ? `${days}d` : days === 0 ? 'today' : `${Math.abs(days)}d late`})} +
+ {obligations.map((ob) => { + const days = ob.due_date ? daysUntil(ob.due_date) : null; + return ( + +
+
+

{ob.payee}

+

+ {ob.category} + {ob.due_date && ( + + {' — '} + {days !== null && days < 0 + ? {Math.abs(days)}d late + : days === 0 + ? Due today + : `Due ${formatDate(ob.due_date)}` + } - ) : ( - - )} -

- - {ob.status} - - - - {ob.urgency_score ?? '—'} - - - {ob.status !== 'paid' && ( - )} -
+

+
+
+

{formatCurrency(ob.amount_due)}

+ + {ob.status} + + {ob.status !== 'paid' && ( + handleMarkPaid(ob.id)} + loading={payingId === ob.id} + /> + )} +
+
+ + ); + })} {obligations.length === 0 && ( -

No obligations found

+

No obligations found

)}
diff --git a/ui/src/pages/CashFlow.tsx b/ui/src/pages/CashFlow.tsx index 56cdc98..b7c5cd7 100644 --- a/ui/src/pages/CashFlow.tsx +++ b/ui/src/pages/CashFlow.tsx @@ -1,6 +1,10 @@ import { useEffect, useState } from 'react'; import { api, type CashflowProjection, type ProjectionResult, type ScenarioResult, type Obligation } from '../lib/api'; +import { Card } from '../components/ui/Card'; +import { MetricCard } from '../components/ui/MetricCard'; +import { ActionButton } from '../components/ui/ActionButton'; import { formatCurrency, formatDate } from '../lib/utils'; +import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, ReferenceLine } from 'recharts'; export function CashFlow() { const [projections, setProjections] = useState([]); @@ -36,8 +40,8 @@ export function CashFlow() { setSummary(result); const proj = await api.getCashflowProjections(); setProjections(proj); - } catch (e: any) { - setError(e.message); + } catch (e: unknown) { + setError(e instanceof Error ? e.message : 'Generation failed'); } finally { setGenerating(false); } @@ -58,177 +62,142 @@ export function CashFlow() { try { const result = await api.runCashflowScenario(Array.from(deferIds)); setScenario(result); - } catch (e: any) { - setError(e.message); + } catch (e: unknown) { + setError(e instanceof Error ? e.message : 'Scenario failed'); } }; - if (error && projections.length === 0) { - return ( -
-

Failed to load cash flow data

-

{error}

- -
- ); - } - - // Parse projections into chart-friendly format - const entries = projections.map((p) => ({ - date: p.projection_date, + // Chart data + const chartData = projections.map((p) => ({ + date: new Date(p.projection_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), balance: parseFloat(p.projected_balance), inflow: parseFloat(p.projected_inflow), outflow: parseFloat(p.projected_outflow), - obligations: parseOblArray(p.obligations), - confidence: parseFloat(p.confidence), })); - const balances = entries.map((e) => e.balance); - const minBalance = balances.length ? Math.min(...balances) : 0; - const maxBalance = balances.length ? Math.max(...balances) : 1; - const range = maxBalance - minBalance || 1; - - // Upcoming obligations for scenario panel const upcoming = obligations .filter((o) => o.status === 'pending' || o.status === 'overdue') .sort((a, b) => a.due_date.localeCompare(b.due_date)) .slice(0, 20); + if (error && projections.length === 0) { + return ( +
+

Failed to load cash flow data

+

{error}

+ +
+ ); + } + return (
-

Cash Flow Forecast

-
- {summary && ( -
-
Start: {formatCurrency(summary.starting_balance)}
-
End: {formatCurrency(summary.ending_balance)}
-
Low: {formatCurrency(summary.lowest_balance)}
-
- )} - -
+

Cash Flow Forecast

+
- {loading ? ( -
Loading projections...
- ) : entries.length === 0 ? ( -
-

No projections yet.

- + {summary && ( +
+ + +
+ )} + + {loading ? ( +
Loading projections...
+ ) : chartData.length === 0 ? ( + +

No projections yet.

+ +
) : ( <> - {/* Bar chart */} -
-
- {entries.map((entry, i) => { - const height = ((entry.balance - minBalance) / range) * 100; - const isNegative = entry.balance < 0; - const hasOutflow = entry.outflow > 0; - const opacity = entry.confidence >= 0.8 ? '' : entry.confidence >= 0.6 ? 'opacity-80' : 'opacity-60'; - return ( -
-
-
-

{formatDate(entry.date)}

-

{formatCurrency(entry.balance)}

- {entry.inflow > 0 &&

+{formatCurrency(entry.inflow)} in

} - {entry.outflow > 0 &&

-{formatCurrency(entry.outflow)} out

} - {entry.obligations.map((o, j) => ( -

{o}

- ))} -

{Math.round(entry.confidence * 100)}% confidence

-
-
- ); - })} -
-
- Today - 30 days - 60 days - 90 days -
-
+ {/* Recharts Area Chart */} + + + + + + + + + + + `$${(v / 1000).toFixed(0)}k`} /> + [value != null ? formatCurrency(value) : '', '']} + /> + + + + + {/* Outflows table */} -
-

Projected Outflows

+ +

Projected Outflows

- - - - - - + + + + - {entries.filter((e) => e.outflow > 0).map((entry, i) => ( - - - - - + + + - ))}
DateObligationsOutflowBalance AfterConfidence
DateOutflowBalance After
{formatDate(entry.date)}{entry.obligations.join(', ') || '-'}-{formatCurrency(entry.outflow)} + {chartData.filter((e) => e.outflow > 0).slice(0, 15).map((entry) => ( +
{entry.date}-{formatCurrency(entry.outflow)} {formatCurrency(entry.balance)} {Math.round(entry.confidence * 100)}%
-
+ )} - {/* Scenario: "What if I defer...?" */} -
-

Scenario: What If I Defer?

-

Select obligations to defer and see the impact on your cash position.

+ {/* Scenario Panel */} + +

Scenario: What If I Defer?

+

Select obligations to defer and see the impact on your cash position.

{upcoming.map((ob) => ( -
- + /> {deferIds.size > 0 && ( - )} @@ -236,49 +205,13 @@ export function CashFlow() { {scenario && (
-
-
Without Deferral
-
- {formatCurrency(scenario.original_balance)} -
-
-
-
With Deferral
-
- {formatCurrency(scenario.projected_balance)} -
-
-
-
Savings
-
{formatCurrency(scenario.savings_from_deferral)}
-
-
-
Items Deferred
-
{scenario.deferred_items.length}
-
+ + + +
)} -
- -
-

Legend

-
-
Normal
-
Payment due
-
Negative balance
-
Low confidence (<70%)
-
-
+
); } - -function parseOblArray(val: string): string[] { - if (!val) return []; - try { - const parsed = JSON.parse(val); - return Array.isArray(parsed) ? parsed : []; - } catch { - return []; - } -} diff --git a/ui/src/pages/Dashboard.tsx b/ui/src/pages/Dashboard.tsx index d293171..9ab55b4 100644 --- a/ui/src/pages/Dashboard.tsx +++ b/ui/src/pages/Dashboard.tsx @@ -1,12 +1,15 @@ import { useEffect, useState, useCallback } from 'react'; import { api, type DashboardData, type Obligation, type Recommendation } from '../lib/api'; -import { formatCurrency, formatDate, daysUntil, urgencyColor, urgencyBg, statusBadgeColor } from '../lib/utils'; +import { useFocusMode } from '../lib/focus-mode'; +import { FocusView } from '../components/dashboard/FocusView'; +import { FullView } from '../components/dashboard/FullView'; export function Dashboard() { const [data, setData] = useState(null); const [error, setError] = useState(null); const [payingId, setPayingId] = useState(null); const [executingId, setExecutingId] = useState(null); + const { focusMode } = useFocusMode(); const reload = useCallback(() => { api.getDashboard().then(setData).catch((e) => setError(e.message)); @@ -40,183 +43,20 @@ export function Dashboard() { } }; - if (error) { + if (error && !data) { return (
-

Failed to load dashboard

-

{error}

-

Make sure the API is running: npm run dev

+

Failed to load dashboard

+

{error}

); } if (!data) { - return
Loading...
; + return
Loading...
; } - const { summary, obligations, disputes, deadlines, recommendations } = data; + const viewProps = { data, onPayNow: handlePayNow, onExecute: handleExecute, payingId, executingId }; - return ( -
- {/* Summary Cards */} -
- - - - 0 ? 'text-red-400' : 'text-green-400'} /> -
- - {/* Urgency Banner */} - {obligations.urgent.length > 0 && obligations.urgent[0].urgency_score && obligations.urgent[0].urgency_score >= 50 && ( -
-
-
- - MOST URGENT - -

- {obligations.urgent[0].payee} — {formatCurrency(obligations.urgent[0].amount_due)} -

-

- {obligations.urgent[0].status === 'overdue' - ? `OVERDUE ${Math.abs(daysUntil(obligations.urgent[0].due_date))} days` - : `Due ${formatDate(obligations.urgent[0].due_date)}`} -

-
- -
-
- )} - -
- {/* Urgent Obligations */} -
-

Upcoming Bills

-
- {obligations.urgent.map((ob) => ( -
-
-
= 70 ? 'bg-red-500' : ob.urgency_score && ob.urgency_score >= 50 ? 'bg-orange-500' : ob.urgency_score && ob.urgency_score >= 30 ? 'bg-yellow-500' : 'bg-green-500'}`} /> -
-

{ob.payee}

-

{ob.category} {ob.due_date ? `- Due ${formatDate(ob.due_date)}` : ''}

-
-
-
-

{formatCurrency(ob.amount_due)}

- - {ob.status} - -
-
- ))} - {obligations.urgent.length === 0 && ( -

No pending obligations

- )} -
-
- - {/* Active Disputes */} -
-

Active Disputes

-
- {disputes.map((d) => ( -
-
-
-

{d.title}

-

vs {d.counterparty}

-
- {d.amount_at_stake && ( - {formatCurrency(d.amount_at_stake)} - )} -
- {d.next_action && ( -

Next: {d.next_action}

- )} -
- ))} - {disputes.length === 0 && ( -

No active disputes

- )} -
-
-
- -
- {/* Legal Deadlines */} -
-

Legal Deadlines

-
- {deadlines.map((dl) => { - const days = daysUntil(dl.deadline_date); - return ( -
-
-

{dl.title}

-

{dl.case_ref}

-
-
-

- {days > 0 ? `${days}d` : days === 0 ? 'TODAY' : `${Math.abs(days)}d ago`} -

-

{formatDate(dl.deadline_date)}

-
-
- ); - })} - {deadlines.length === 0 && ( -

No upcoming deadlines

- )} -
-
- - {/* AI Recommendations */} -
-

AI Recommendations

-
- {recommendations.map((rec) => ( -
-
-
- {rec.rec_type} - {rec.title} -
- #{rec.priority} -
-

{rec.reasoning}

- {rec.action_type && ( - - )} -
- ))} - {recommendations.length === 0 && ( -

No recommendations yet — AI triage will generate these

- )} -
-
-
-
- ); -} - -function SummaryCard({ label, value, color }: { label: string; value: string; color: string }) { - return ( -
-

{label}

-

{value}

-
- ); + return focusMode ? : ; } diff --git a/ui/src/pages/Disputes.tsx b/ui/src/pages/Disputes.tsx index 07bd3c1..15f1ed1 100644 --- a/ui/src/pages/Disputes.tsx +++ b/ui/src/pages/Disputes.tsx @@ -1,15 +1,24 @@ import { useEffect, useState, useCallback } from 'react'; import { api, type Dispute, type Correspondence } from '../lib/api'; +import { Card } from '../components/ui/Card'; +import { ActionButton } from '../components/ui/ActionButton'; +import { ProgressDots } from '../components/ui/ProgressDots'; import { formatCurrency, formatDate } from '../lib/utils'; +const DISPUTE_STAGES = ['filed', 'response_pending', 'in_review', 'resolved']; + +function disputeStageIndex(status: string): number { + const idx = DISPUTE_STAGES.indexOf(status); + return idx >= 0 ? idx : 0; +} + export function Disputes() { const [disputes, setDisputes] = useState([]); const [error, setError] = useState(null); - const [correspondenceFor, setCorrespondenceFor] = useState(null); - const [documentsFor, setDocumentsFor] = useState(null); - const [statusFor, setStatusFor] = useState(null); + const [expandedId, setExpandedId] = useState(null); const [correspondenceList, setCorrespondenceList] = useState([]); const [documentList, setDocumentList] = useState<{ id: string; filename: string | null; doc_type: string; created_at: string }[]>([]); + const [activePanel, setActivePanel] = useState<'correspondence' | 'documents' | null>(null); const [newCorrespondence, setNewCorrespondence] = useState({ direction: 'outbound', channel: 'email', subject: '', content: '' }); const [saving, setSaving] = useState(false); @@ -19,135 +28,140 @@ export function Disputes() { useEffect(() => { reload(); }, [reload]); - const openCorrespondence = async (disputeId: string) => { - setCorrespondenceFor(disputeId); - try { - const detail = await api.getDispute(disputeId); - setCorrespondenceList(detail.correspondence || []); - } catch { setCorrespondenceList([]); } - }; - - const openDocuments = async (disputeId: string) => { - setDocumentsFor(disputeId); + const [panelError, setPanelError] = useState(null); + + const togglePanel = async (disputeId: string, panel: 'correspondence' | 'documents') => { + if (expandedId === disputeId && activePanel === panel) { + setExpandedId(null); + setActivePanel(null); + setPanelError(null); + return; + } + setExpandedId(disputeId); + setActivePanel(panel); + setPanelError(null); try { const detail = await api.getDispute(disputeId); - setDocumentList(detail.documents || []); - } catch { setDocumentList([]); } + if (panel === 'correspondence') setCorrespondenceList(detail.correspondence || []); + else setDocumentList(detail.documents || []); + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : 'Failed to load'; + console.error(`[Disputes] ${panel} load failed for ${disputeId}:`, msg, e); + setPanelError(`Unable to load ${panel}: ${msg}`); + if (panel === 'correspondence') setCorrespondenceList([]); + else setDocumentList([]); + } }; const submitCorrespondence = async () => { - if (!correspondenceFor || !newCorrespondence.subject.trim()) return; + if (!expandedId || !newCorrespondence.subject.trim()) return; setSaving(true); try { - await api.addCorrespondence(correspondenceFor, newCorrespondence); + await api.addCorrespondence(expandedId, newCorrespondence); setNewCorrespondence({ direction: 'outbound', channel: 'email', subject: '', content: '' }); - const detail = await api.getDispute(correspondenceFor); + const detail = await api.getDispute(expandedId); setCorrespondenceList(detail.correspondence || []); } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Failed to add correspondence'); - } finally { setSaving(false); } + } finally { + setSaving(false); + } }; - if (error) return

{error}

; + if (error) return

{error}

; - const priorityColor = (p: number) => { - if (p <= 1) return 'bg-red-600'; - if (p <= 3) return 'bg-orange-600'; - return 'bg-yellow-600'; + const priorityUrgency = (p: number): 'red' | 'amber' | 'green' => { + if (p <= 1) return 'red'; + if (p <= 3) return 'amber'; + return 'green'; }; return ( -
-

Active Disputes

+
+

Active Disputes

-
+
{disputes.map((d) => ( -
-
-
-
- + +
+
+
+ P{d.priority} - + {d.dispute_type}
-

{d.title}

-

vs {d.counterparty}

+

{d.title}

+

vs {d.counterparty}

{d.amount_at_stake && ( -
-

At Stake

-

{formatCurrency(d.amount_at_stake)}

+
+

At Stake

+

{formatCurrency(d.amount_at_stake)}

)}
+ + {d.description && ( -

{d.description}

+

{d.description}

)} {d.next_action && ( -
-

Next Action

-

{d.next_action}

+
+

Next Action

+

{d.next_action}

{d.next_action_date && ( -

By {formatDate(d.next_action_date)}

+

By {formatDate(d.next_action_date)}

)}
)} -
- - - +
+ togglePanel(d.id, 'correspondence')} + /> + togglePanel(d.id, 'documents')} + />
{/* Correspondence Panel */} - {correspondenceFor === d.id && ( -
-
-

Correspondence

- -
+ {expandedId === d.id && activePanel === 'correspondence' && ( +
+

Correspondence

+ {panelError && ( +

{panelError}

+ )} {correspondenceList.length > 0 ? (
{correspondenceList.map((c) => ( -
-
+
+
{c.direction} via {c.channel} {formatDate(c.sent_at)}
- {c.subject &&

{c.subject}

} - {c.content &&

{c.content}

} + {c.subject &&

{c.subject}

} + {c.content &&

{c.content}

}
))}
) : ( -

No correspondence yet

+

No correspondence yet

)}
setNewCorrespondence({ ...newCorrespondence, channel: e.target.value })} - className="bg-[#161822] border border-gray-700 rounded px-2 py-1 text-xs text-white" + className="bg-card-bg border border-card-border rounded-lg px-2 py-1 text-xs text-card-text" > @@ -168,66 +182,52 @@ export function Disputes() { placeholder="Subject" value={newCorrespondence.subject} onChange={(e) => setNewCorrespondence({ ...newCorrespondence, subject: e.target.value })} - className="w-full bg-[#161822] border border-gray-700 rounded px-3 py-1.5 text-sm text-white" + className="w-full bg-card-bg border border-card-border rounded-lg px-3 py-1.5 text-sm text-card-text" />