diff --git a/CLAUDE.md b/CLAUDE.md index 18cb893..7cbe7a4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,18 +7,19 @@ Handles attendance, invoicing, leave/trips, projects, vehicles, and HR operation ## Tech Stack -| Layer | Technology | -| -------------- | ------------------------------------------------------------- | -| Runtime | Node.js, TypeScript 5.9.3 (strict) | -| HTTP Framework | Fastify 5.8.2 | -| ORM | Prisma 6.19.2 → MySQL | -| Auth | JWT (HS256, 15 min) + TOTP 2FA (RFC 6238, otpauth) + bcryptjs | -| Validation | Zod 4.3.6 | -| Frontend | React 18.3.1 + Vite 8.0.0 + Material UI v7 (Emotion) | -| Testing | Vitest 4.1.0 + Supertest | -| PDF | Puppeteer 24.x | -| Email | nodemailer 8.x | -| Cron | node-cron 4.x | +| Layer | Technology | +| -------------- | -------------------------------------------------------------- | +| Runtime | Node.js, TypeScript 5.9.3 (strict) | +| HTTP Framework | Fastify 5.8.2 | +| ORM | Prisma 6.19.2 → MySQL | +| Auth | JWT (HS256, 15 min) + TOTP 2FA (RFC 6238, otpauth) + bcryptjs | +| Validation | Zod 4.3.6 | +| Frontend | React 18.3.1 + Vite 8.0.0 + Material UI v7 (Emotion) | +| Testing | Vitest 4.1.0 + Supertest | +| PDF | Puppeteer 24.x | +| Email | nodemailer 8.x | +| Cron | node-cron 4.x | +| AI | @anthropic-ai/sdk 0.102 — Claude Sonnet 4.6 ("Odin" assistant) | --- @@ -35,21 +36,21 @@ src/ ├── utils/ # totp.ts, pdf.ts, email.ts, audit.ts, formatters, etc. ├── config/ # env.ts (config singleton, Date.toJSON override) ├── types/ # index.ts (AuthData, JwtPayload, ApiResponse, re-exports from Prisma) -├── admin/ # React 18 + Material UI frontend (~105 .tsx files) +├── admin/ # React 18 + Material UI frontend (~114 .tsx files) │ ├── AdminApp.tsx # Router + lazy-loaded pages │ ├── theme.ts # MUI theme — light/dark color schemes, tokens, component defaults │ ├── GlobalStyles.tsx # App-wide global styles via MUI (reset, typography, utilities) │ ├── ui/ # MUI component kit (AppShell, Button, DataTable, Modal, …) — pages import from here │ ├── context/ # AuthContext, AlertContext (ThemeContext lives in src/context/) -│ ├── components/ # Non-page components: RichEditor (Quill), PlanGrid, file manager, dashboard/ + warehouse/ widgets +│ ├── components/ # Non-page components: RichEditor (Quill), PlanGrid, file manager, dashboard/ + warehouse/ widgets, odin/ (AI chat) │ ├── pages/ # One file per page/feature │ ├── lib/ # React Query options & mutations (queries/) + shared label maps │ ├── hooks/ # usePaginatedQuery, useTableSort, useDebounce, useReducedMotion, … │ └── utils/ # api.ts (fetch wrapper with token refresh), formatters, helpers -└── __tests__/ # Vitest tests (~14 files: auth, numbering, warehouse, plan, invoices, …) +└── __tests__/ # Vitest tests (17 files: auth, numbering, warehouse, plan, invoices, ai/Odin, received-invoices VAT, …) prisma/ -├── schema.prisma # 49 models, MySQL, snake_case columns +├── schema.prisma # 52 models, MySQL, snake_case columns └── migrations/ # Applied migrations dist/ # Compiled server (CommonJS, ES2022) @@ -240,7 +241,7 @@ if ("error" in body) return error(reply, body.error, 400); Tests live in `src/__tests__/`. They use Vitest + Supertest against a real test database (`.env.test`). -- The suite spans ~14 files / 152 tests — auth, numbering, warehouse, plan, invoices, exchange-rates, schema coercion, NAS file manager, env, manual-create. Server-side only (no component tests yet). +- The suite spans 17 files / 195 tests — auth, numbering, warehouse, plan, invoices, exchange-rates, schema coercion, NAS file manager, env, manual-create, AI/Odin (conversation CRUD, isolation, auto-title, cascade, budget), received-invoices VAT. Server-side only (no component tests yet). - Use `buildApp()` helper to spin up the Fastify instance for tests. - Tests use `vitest.config.ts` with `environment: 'node'` and 15s timeout. - **Do not mock Prisma** — tests hit a real database to catch schema/query bugs. @@ -305,6 +306,19 @@ The admin frontend is **fully on Material UI v7** (Emotion). The old ~7,100 line --- +## AI Assistant — "Odin" (shipped v2.1.5) + +Admins-only Claude assistant on the `/odin` page (sidebar nav item "Odin"). **Phase-1 scope is invoice import ONLY** — general chat is guarded off in `OdinChat.submit` to avoid spending API credits: a text-only message gets a canned reply and makes NO AI call. Removing that one guard re-enables the `/chat` path for a later "general assistant" phase. + +- **SDK / model:** `@anthropic-ai/sdk` → `claude-sonnet-4-6`. Server-side only — `ANTHROPIC_API_KEY` in `.env` (already set on prod; **don't touch env, the user manages it**). The whole feature self-hides when the key is absent (`/ai/usage` returns `configured:false`). +- **Backend:** `src/services/ai.service.ts` (chat, `extractInvoice` [PDF→structured-output JSON], cost/budget tracking, conversation CRUD with server-side auto-title), `src/routes/admin/ai.ts` (`/api/admin/ai/*`: usage, budget, chat, extract-invoices, conversations[/:id/messages]), `src/schemas/ai.schema.ts`. Every route `requirePermission("ai.use")` (granted to **admin only** via migration). +- **Data:** `ai_usage` (per-call token-cost ledger), `ai_conversations` + `ai_chat_messages` (per-user, FK cascade, auto-title from first message), `company_settings.ai_monthly_budget_usd` (default $50; `assertBudgetAvailable` → 402 at the cap). +- **Frontend:** `src/admin/pages/Odin.tsx` → `src/admin/components/odin/`: `OdinChat` (orchestrator + state + the proven submit/extract/save logic), `OdinSidebar` (conversation list; inline ≥md, slide-in Drawer below md), `OdinThread` (messages, framer-motion entrance, Newsreader serif greeting hero), `OdinComposer` (claude-style rounded multiline pill, attach/send icons), `InvoiceReviewCard`, `OdinMark` (animated brand mark — CSS keyframes via `styled`, not inline style; opacity-glow fallback under reduced-motion), `types.ts`. Queries in `src/admin/lib/queries/ai.ts`. `Fraunces`→`Newsreader` font added to `index.html`. +- **Invoice flow:** attach PDF(s) → `extract-invoices` → editable review cards → save to the EXISTING `POST /received-invoices`. **`received_invoices.amount` is GROSS (VAT-inclusive)** — VAT is back-calculated via `vatFromGross` in `received-invoices.ts` (`amount * rate/(100+rate)`), used by both the manual form and the AI import. The save button is gated on `invoices.create`. +- **Phase 2 (general assistant — NOT built):** forward-looking design notes in `docs/superpowers/specs/2026-06-08-odin-phase2-general-assistant-notes.md` (tool-use over services, structured message-block storage, per-action authorization). Phase-1 spec/plan also under `docs/superpowers/`. + +--- + ## Database Conventions - All models use `snake_case` column names; Prisma maps to camelCase in TypeScript.