diff --git a/.claude/rules/api-routes.md b/.claude/rules/api-routes.md new file mode 100644 index 00000000..62be9c68 --- /dev/null +++ b/.claude/rules/api-routes.md @@ -0,0 +1,53 @@ +--- +paths: + - "app/api/**" +--- + +# API Route Pattern + +Use the `/erp-api-route` skill when scaffolding new endpoints. + +```typescript +import { createClient } from '@/lib/supabase/server' +import { NextResponse } from 'next/server' +import { ensureInitialized } from '@/lib/init' +import { validateBody } from '@/lib/api/validate' +import { MySchema } from '@/lib/api/schemas' + +ensureInitialized() // Module-level — loads extensions for event emission + +export async function POST(request: Request) { + const supabase = await createClient() + const { data: { user } } = await supabase.auth.getUser() + if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + + const result = await validateBody(request, MySchema) + if (!result.success) return result.response + + // Business logic... always filter by company_id (defense in depth alongside RLS) + return NextResponse.json({ data: result }) +} +``` + +- Dynamic route params: `{ params }: { params: Promise<{ id: string }> }` (Next.js 16 — params are async). +- Response shapes: `{ data }` for success, `{ error }` for failures. +- Zod schemas in `lib/api/schemas.ts` — 100+ schemas with shared primitives (uuid, isoDate, accountNumber, nonNegativeAmount). +- Routes that emit events must call `ensureInitialized()` at module level. +- API-key auth uses `createServiceClientNoCookies()`; every query still filters by `company_id`. + +## Endpoint map (`app/api/`) + +- `/api/bookkeeping/*` — accounts, fiscal periods, journal entries (CRUD/reverse/correct), mapping rules, voucher gaps +- `/api/invoices/*`, `/api/supplier-invoices/*` — CRUD + state transitions +- `/api/transactions/*` — categorize, describe, book, match-{invoice,supplier-invoice}, batch, AI suggestions +- `/api/customers/*`, `/api/suppliers/*` — CRUD +- `/api/documents/*` — CRUD, versions, link, match-sweep, verify cron +- `/api/reports/*` — report endpoints (GL, TB, BS, IS, AR/supplier ledger, VAT, SIE, INK2, NE-bilaga, KPI, audit, continuity, monthly, full-archive, salary, vacation, avgifter) +- `/api/salary/*` — employees, payroll-config, tax-tables, KU, runs +- `/api/import/*` — bank-file, SIE (parse/execute/mappings) +- `/api/reconciliation/bank/*`, `/api/settings/*`, `/api/company/*`, `/api/team/*` +- `/api/deadlines/*`, `/api/tax-deadlines/*` — CRUD + crons +- `/api/pending-operations/*`, `/api/events/*`, `/api/audit-trail/*` +- `/api/calendar/feed/[token]`, `/api/mcp-oauth/*`, `/api/support/contact`, `/api/account/delete` +- `/api/log`, `/api/health`, `/api/vat/validate`, `/api/currency/rate`, `/api/sandbox/*` +- `/api/extensions/ext/[...path]` — dynamic extension routes (catch-all → `/api/extensions/ext/{extensionId}/{routePath}`, path params as `_paramName` query) diff --git a/.claude/rules/bookkeeping.md b/.claude/rules/bookkeeping.md new file mode 100644 index 00000000..e18f8248 --- /dev/null +++ b/.claude/rules/bookkeeping.md @@ -0,0 +1,44 @@ +--- +paths: + - "lib/bookkeeping/**" + - "lib/core/**" + - "lib/reports/**" + - "lib/vat/**" + - "lib/invoices/**" + - "lib/salary/**" +--- + +# Bookkeeping Domain Reference + +For Swedish accounting-law questions, use the domain skills (`swedish-vat`, `swedish-accounting-compliance`, `swedish-year-end-closing`, etc.). The accounting guard rails in the root `CLAUDE.md` always apply. + +## Core Services (`lib/core/`) + +- `bookkeeping/period-service.ts` — Fiscal period lifecycle management (open, close, lock) +- `bookkeeping/year-end-service.ts` — Year-end closing procedures +- `bookkeeping/storno-service.ts` — Reversal/correction entry generation +- `tax/tax-code-service.ts` — Tax code definitions and rates +- `audit/audit-service.ts` — Audit trail and compliance logging +- `documents/document-service.ts` — Document attachment lifecycle (WORM storage with version chains) + +## Key BAS Accounts + +`1510` Accounts receivable | `1930` Business bank account | `2013` Private withdrawals (EF) | `2440` Accounts payable | `2611`/`2621`/`2631` Output VAT 25%/12%/6% | `2641` Input VAT | `2645` Calculated input VAT (EU) | `2893` Shareholder loan (AB) | `3001`/`3002`/`3003` Revenue 25%/12%/6% | `3305`/`3308` Export/EU service revenue + +BAS data (`lib/bookkeeping/bas-data/`): full BAS 2026 chart by class (1–8) + SRU mapping. Account numbers are **strings** (`'1930'`, never `1930`). + +## VAT Treatments + +`standard_25`, `reduced_12`, `reduced_6`, `reverse_charge`, `export`, `exempt` + +Invoice items support individual `vat_rate` values (mixed-rate invoices). Use `getAvailableVatRates(customerType, vatNumberValidated)` from `lib/invoices/vat-rules.ts`. VIES validation via `lib/vat/vies-client.ts`. + +## VAT Declaration Rutor (SKV 4700) + +`VatDeclarationRutor` type maps to momsdeklaration: +- **Ruta 05**: Domestic taxable sales (3001+3002+3003) +- **Ruta 06/07**: Unused, always 0 +- **Ruta 10/11/12**: Output VAT 25%/12%/6% (2611/2621/2631) +- **Ruta 39/40**: EU services / Export (3308/3305) +- **Ruta 48**: Input VAT (2641/2645) +- **Ruta 49**: Moms att betala/återfå = (10+11+12+30+31+32+60+61+62) − 48 diff --git a/.claude/rules/database.md b/.claude/rules/database.md new file mode 100644 index 00000000..18b0d0d4 --- /dev/null +++ b/.claude/rules/database.md @@ -0,0 +1,72 @@ +--- +paths: + - "supabase/migrations/**" + - "tests/pg/**" +--- + +# Database & Migrations + +Use the `/supabase-migration` skill for new migrations. + +**Location**: `supabase/migrations/` — 330+ files. Early migrations use sequential numbering (`20240101000001`–`20240101000038`), later ones use real timestamps. + +## Migration Rules + +1. Enable RLS + policies using `user_company_ids()` for company-scoped data +2. Add `updated_at` trigger via `update_updated_at_column()` +3. UUID PKs: `DEFAULT uuid_generate_v4()` +4. Company ownership: `company_id UUID REFERENCES companies NOT NULL` + `user_id UUID REFERENCES auth.users ON DELETE CASCADE NOT NULL` +5. Never modify existing migrations — create new ones +6. Never modify enforcement triggers (migration 017) — legally required +7. Apply via Supabase MCP `apply_migration` +8. Always end with `NOTIFY pgrst, 'reload schema'` when altering table structure + +**pg-real tests**: any PR touching a trigger/RPC/RLS/DEFERRABLE must include or extend a `*.pg.test.ts`. Parallel Vitest project against real Postgres (CI: `supabase/postgres:15`, migrations replayed). Local: `npm run test:pg`. Helpers: `tests/pg/setup.ts` (`getPool()`, `withUserContext()`), `tests/pg/fixtures.ts` (`seedCompany()`, `insertDraftJournalEntry()`, etc.). + +## Key Tables (~60) + +- **Multi-tenant**: `companies`, `company_members`, `company_invitations`, `teams`, `team_members`, `team_invitations`, `user_preferences`, `profiles` +- **Bookkeeping**: `chart_of_accounts`, `fiscal_periods`, `journal_entries`, `journal_entry_lines`, `account_balances`, `voucher_sequences`, `voucher_gap_explanations` +- **Invoicing**: `customers`, `invoices`, `invoice_items`, `invoice_payments`, `invoice_inbox_items` +- **Suppliers**: `suppliers`, `supplier_invoices`, `supplier_invoice_items` +- **Banking**: `bank_connections`, `transactions`, `bank_file_imports`, `payment_match_log` +- **Documents**: `document_attachments` (WORM), `receipts`, `receipt_line_items` +- **Settings**: `company_settings`, `mapping_rules`, `categorization_templates`, `booking_template_library`, `extension_data` +- **Dimensions**: `cost_centers`, `projects` +- **Tax/Deadlines**: `tax_rates`, `tax_table_rates`, `deadlines`, `calendar_feeds`, `skatteverket_tokens` +- **API/Auth**: `api_keys`, `oauth_used_codes`, `bankid_identities` +- **Audit/Ops**: `audit_log` (immutable), `event_log` (30d TTL), `pending_operations`, `processing_history`, `ai_usage_tracking`, `automation_webhooks` +- **Inbox**: `invoice_inbox_items`, `company_inboxes`, `email_connections` +- **Salary**: `employees`, `salary_runs`, `salary_run_employees`, `salary_line_items`, `salary_payroll_config`, `agi_declarations` +- **Providers**: `provider_consents`, `provider_consent_tokens`, `provider_otc` +- **Agent**: `agent_atom_registry` (inlined skill bodies — see below) +- **Other**: `sandbox_users` + +## Key RPC Functions + +- `create_company_with_owner()` — Atomic company + owner creation +- `commit_journal_entry()` — Atomic draft→posted with voucher number +- `next_voucher_number()` — Concurrent-safe voucher generation +- `detect_voucher_gaps()` — BFNAR 2013:2 gap detection +- `generate_invoice_number()`, `get_next_arrival_number()`, `generate_delivery_note_number()` — Sequence generators +- `seed_chart_of_accounts()` — BAS chart seeding per entity type +- `validate_and_increment_api_key()` — Atomic rate limiting +- `user_company_ids()` — RLS helper returning user's company IDs +- `get_unlinked_1930_lines()` — Bank reconciliation helper +- `cleanup_sandbox_user()`, `cleanup_expired_sandbox_users()` — Sandbox lifecycle + +## Key Triggers + +- `check_journal_entry_balance()` — Debit must equal credit +- `enforce_journal_entry_immutability()` — Posted entries cannot be modified +- `enforce_period_lock()` — No entries in closed/locked periods +- `enforce_company_lock_date()` — Company-wide bookkeeping lock date +- `block_document_deletion()` — WORM compliance +- `enforce_retention_journal_entries()` — 7-year retention +- `audit_log_immutable()` — Audit log cannot be modified +- `write_audit_log()` — Auto-audit on DML operations +- `sync_team_member_to_companies()` — Auto-sync team→company membership + +## Agent skill bodies (`agent_atom_registry`) + +Skill content is authored in `.claude/skills/**/SKILL.md` and inlined into the DB `body` column at runtime (not read from disk — that doesn't bundle on Vercel/Docker). After editing any atom SKILL.md, run `npm run skills:generate` to emit a new `*_seed_agent_atom_bodies.sql` migration and commit it; `npm run skills:check` (wired into CI) fails the build if you forget. Only the curated tiers become atoms — `swedish-*` (horizontal), `industry/` (vertical), `modifier/` (modifier); other Claude Code skills never become atoms. The MCP server exposes only atoms with `mcp_exposed = true`. diff --git a/.claude/rules/design.md b/.claude/rules/design.md new file mode 100644 index 00000000..8153c849 --- /dev/null +++ b/.claude/rules/design.md @@ -0,0 +1,104 @@ +--- +paths: + - "app/**" + - "components/**" +--- + +# Design Context & Design System + +Always use the `/frontend-design` skill for new UI. The conventions below are locked — deviating from them on existing pages is a regression. + +## Users + +Swedish sole traders (enskild firma) and small business owners (aktiebolag) who manage their own bookkeeping. They are not accountants — they are professionals (consultants, freelancers, shop owners) who want to stay compliant without hiring one. They use Accounted in short, focused sessions: sending an invoice, categorizing bank transactions, filing a VAT declaration. Speed and clarity matter. + +## Brand & Aesthetic + +**Editorial monochrome.** Paper-white surfaces, hairline borders, serif headlines. The interface should feel like a well-made instrument — considered, quiet, confident. Anti-references: enterprise software (SAP/Oracle density), neon SaaS coldness. + +- **Palette**: Achromatic foundation. Pure white background, warm beige (`40 11% 89%`) for chips / active sidebar / hover / secondary buttons. Achromatic primary (no cool tint). Semantic colors (`--success` sage, `--warning` ochre, `--destructive` terracotta) exist but are **data-only** — they appear in charts and financial numbers (positive/negative deltas), never as chrome backgrounds. In chrome, only `--destructive` survives. +- **Typography**: Hedvig Letters Serif for display headings, Geist (sans) for body, forms, and tables. Hedvig is single-weight (400) — do not apply `font-medium` to display text; its natural high-contrast strokes carry the weight. Tabular numbers everywhere financial data appears. +- **Surfaces**: Cards sit flat on the page — no shadow, full-opacity hairline border (`border-border`), `rounded-lg` (8px). Card background matches page background; the border carries hierarchy. Dark mode drops the warm tint from secondary for a pure-gray mood shift; light mode keeps the beige. +- **Spacing**: Generous whitespace. Dense data (tables, ledgers) uses tighter spacing but never feels cramped. +- **Motion**: Functional, not decorative. No press-scale, no hover-lift, no spring overshoot. Hover state is a flat background shift (`bg-secondary/60`). `transition-colors duration-150` is the default. Stagger animations on list entry are fine. Respect `prefers-reduced-motion` (already wired). +- **Icons**: Lucide — 15px in navigation, slightly larger in empty states. + +## Design Principles + +1. Clarity over cleverness — Swedish labels, obvious hierarchy. +2. Earned minimalism — remove what doesn't serve the task, keep compliance context. +3. Numbers are first-class — tabular-nums, alignment, positive/negative clarity. +4. Trust through consistency. +5. Speed is a feature — optimize for the 90-second session. + +## Accessibility + +WCAG AA (4.5:1 text, 3:1 UI). Keyboard-navigable + visible focus rings. Respect `prefers-reduced-motion`. Color never sole state indicator. Touch targets ≥40px (44px for mobile-critical). Icon-only buttons need `aria-label`. + +## Design System Tokens + +**Spacing scale.** Only use Tailwind values `1, 2, 3, 4, 6, 8, 10, 12`. **Forbidden:** `2.5`, `5`, hardcoded pixels in page logic. + +| Token | Tailwind | Use for | +|---|---|---| +| 4 | `1` | icon padding | +| 8 | `2` | tight inline gaps | +| 12 | `3` | dense list rows, badge gaps | +| 16 | `4` | default form / control / grid gap | +| 24 | `6` | **card padding default** (`p-6`) | +| 32 | `8` | **between page sections** (`space-y-8` on page root) | +| 40 | `10` | hero spacing | +| 48 | `12` | top of page after header | + +Compact metric cards (e.g. dashboard tiles, salary KPI row) use `p-4`. Detail cards use `p-6`. Never mix `p-5`. + +**Layout.** +- Sidebar width: `md:w-64` (256px). Main content offset: `md:pl-64`. +- Main container: `max-w-5xl mx-auto px-5 py-8 md:px-8 md:py-10` (via `components/dashboard/MainContainer.tsx`). +- Page root: `
`. + +**Primitives — always use these, don't hand-roll.** + +| Need | Component | Notes | +|---|---|---| +| Page title + action | `components/ui/page-header.tsx` `PageHeader` | Use this, not bespoke `

` + `

` blocks. Drop the `description` prop when it just paraphrases the title. | +| Data table | `components/ui/table.tsx` `Table / TableHeader / TableHead / TableRow / TableCell` | Header style is baked in: `text-[11px] font-medium uppercase tracking-wider text-muted-foreground`. Wrap in `` when the table is a card's primary content. Add `tabular-nums` to numeric cells. | +| Status indicator | `components/ui/badge.tsx` `` | Variants: `default / secondary / success / warning / destructive / outline`. **Never** use raw Tailwind colors (`bg-blue-100`, `bg-emerald-500/10`, etc.) for status. Map status → variant via a small `Record` per feature. | +| No-data state | `components/ui/empty-state.tsx` `EmptyState` | Don't hand-roll `

`. Preset variants exist (`EmptyInvoices`, `EmptyCustomers`, `EmptyTransactions`, etc.). | +| Loading placeholder | `components/ui/skeleton.tsx` `` | Don't hand-roll `bg-muted rounded animate-pulse` divs. | +| Inline help / formulas | `components/ui/info-tooltip.tsx` `InfoTooltip` | Hover-revealed; don't use always-visible info buttons. | +| Fiscal year picker | `components/common/FiscalYearSelector.tsx` | Don't use raw `` duplicating desktop tabs in code — use a single Tabs primitive or a single grouped `Select`. +- Hand-rolled icon buttons smaller than `h-10 w-10`. Use shadcn `Button size="icon"`. +- Color-coded status using full-rainbow Tailwind palette (`bg-amber-100`, `bg-emerald-500/10`, etc.). Use Badge variants tied to the brand palette. +- `shadow-sm` / `shadow-md` / `shadow-lg` on cards, buttons, or list items. The aesthetic is flat-with-hairlines — surfaces use `border-border`, not elevation. Shadows survive only on dialogs/popovers/dropdowns (anything that overlays the page). +- `active:scale-[...]` on buttons. Buttons do not bounce. +- `bg-gradient-to-*` on page or card backgrounds. Flat surfaces only. +- `font-medium` on display elements (`font-display`, h1/h2/h3, CardTitle, PageHeader title). Hedvig is single-weight by design. +- `rounded-xl` (12px) on cards. Cards are `rounded-lg` (8px). `rounded-xl` survives only on prominent hero-style surfaces if absolutely needed. +- Opacity-suffixed border classes (`border-border/30`, `border-border/60`) on cards and primary surfaces. Use full-opacity `border-border` — the new border token is calibrated for that. diff --git a/.claude/rules/i18n.md b/.claude/rules/i18n.md new file mode 100644 index 00000000..1c8da940 --- /dev/null +++ b/.claude/rules/i18n.md @@ -0,0 +1,56 @@ +--- +paths: + - "app/**" + - "components/**" + - "messages/**" + - "lib/email/**" + - "lib/invoices/**" + - "lib/reports/**" + - "lib/salary/**" +--- + +# i18n + +The app is Swedish-first and bilingual (Swedish + English) for UI chrome. Locale is per-user on `user_preferences.locale` (`'sv' | 'en'`, default `'sv'`), resolved server-side via next-intl. The user picker lives at `/settings/account`. + +**Pattern for new UI:** + +```tsx +// Server component +import { getTranslations } from 'next-intl/server' +const t = await getTranslations('namespace') + +// Client component +'use client' +import { useTranslations } from 'next-intl' +const t = useTranslations('namespace') + +// In JSX + +``` + +Add new strings to both `messages/sv.json` and `messages/en.json` under the matching namespace (`common`, `nav`, `auth`, `settings`, `empty`, etc.). Never ship an English key without a Swedish counterpart — Swedish is the default and the fallback. + +**Locale-aware formatters:** +- `formatCurrency(amount)` — stays SEK with sv-SE conventions in BOTH locales (Swedish accounting standard, not a UI string). +- `formatDate(date)` — ISO `yyyy-MM-dd`, locale-independent. +- `formatDateLong(date, locale)` — accepts locale. In client components use `useFormat()` (`lib/hooks/use-format.ts`) which pulls the active locale. + +**Error messages:** `getErrorMessage(err, { locale, context })` from `lib/errors/get-error-message.ts` is bilingual on the primary maps (Postgres codes, HTTP statuses, context fallbacks, generic fallback). The structured error envelope (`{ error: { code, message, message_en } }`) already carries both; the function picks the right one from `locale`. Pass `useLocale()` / `getLocale()` as the locale arg. + +**Stays Swedish — do NOT translate:** + +| Surface | Reason | +|---|---| +| Invoice PDFs (`lib/invoices/pdf-template.tsx`) | Customer-facing — driven by `customer.language` (`sv` default, `en` opt-in). The template's chrome translates; statutory chapter refs (ML 17 kap 24§, ML 3 kap.) stay intact in both locales. | +| Customer email templates (`lib/email/invoice-templates.ts`, `reminder-templates.ts`) | Same — `customer.language` drives the output. `reminder-templates.ts` is still Swedish-only; mirror the PDF/invoice-templates approach if you add English here. | +| Year-end wizard (`app/(dashboard)/bookkeeping/year-end/page.tsx`) | Statutory bokslut terminology; English would be misleading | +| Journal entry editor (`app/(dashboard)/bookkeeping/[id]/page.tsx`) | Deeply regulatory (verifikat, voucher numbers, BAS) | +| INK2 / NE-bilaga / SRU (`lib/reports/ink2/**`, `lib/reports/ne-bilaga/**`, `lib/reports/sru-*`) | Skatteverket forms — field codes and labels are statutory | +| SIE export (`lib/reports/sie-export.ts`) | SIE format is Swedish-only by spec (#KONTO, #VER, etc.) | +| BAS chart names (`lib/bookkeeping/bas-data/**`) | Standardized Swedish account names per BAS 2026 | +| VAT declaration ruta labels (`lib/reports/vat-declaration*.ts`) | Momsdeklaration field labels are Skatteverket form labels | +| Salary AGI / KU (`lib/salary/agi*`, `lib/salary/ku*`) | Skatteverket-bound forms | +| Bookkeeping engine domain errors ("Verifikationen balanserar inte", "Bokföringen är låst") | Regulatory concepts; English equivalents would be ambiguous | + +Anything in the table above stays Swedish in BOTH locales. If you find yourself reaching for `t()` inside one of these files, stop and reconsider. diff --git a/.claude/rules/mcp-server.md b/.claude/rules/mcp-server.md new file mode 100644 index 00000000..75311ef6 --- /dev/null +++ b/.claude/rules/mcp-server.md @@ -0,0 +1,22 @@ +--- +paths: + - "extensions/general/mcp-server/**" + - "packages/gnubok-mcp/**" +--- + +# MCP Server + +Accounted exposes its bookkeeping engine as an MCP server for Claude Desktop/Code. + +**MCP extension** (`extensions/general/mcp-server/`): 90+ tools covering transactions, categorization, customers/suppliers, invoices, accounts, fiscal periods, reports (trial balance, GL, BS, IS, AR/supplier ledger, VAT, KPI), reconciliation, salary runs, AGI, year-end, document upload, and loadable skills. JSON-RPC 2.0. Endpoint: `/api/extensions/ext/mcp-server/mcp`. + +**OAuth 2.1** for Claude connectors: `.well-known/oauth-protected-resource` + `.well-known/oauth-authorization-server` discovery; `/api/mcp-oauth/authorize`, `/token` (PKCE), `/register`. Stateless AES-256-GCM auth codes (`lib/auth/oauth-codes.ts`). Single-use via `oauth_used_codes`. Allowlist: `claude.ai/api/*`, `claude.com/api/*`, `localhost`. + +**npm package** (`packages/gnubok-mcp`): Stdio-to-HTTP bridge; users run `npx gnubok-mcp` with API key. + +## Tool authoring conventions (enforced by tests) + +- Every `inputSchema` must declare `additionalProperties: false` at the top level. Guarded by `extensions/general/mcp-server/__tests__/strict-schemas.test.ts`. +- Tool descriptions must be ≤ 280 chars (guarded by `output-schema.test.ts`). No `Args:` / `Returns:` / `Examples:` blocks — those belong in JSON Schema, not description prose. Use agent-native hints like "Use to…" / "Call X first" instead. +- Completion-signal pattern: tools that stage operations return `STAGED_OPERATION_SCHEMA` — `{ staged, risk_level, actor, message, preview, period_status?, next? }`. The `staged: true` boolean is the explicit completion signal; agents must not infer completion from prose. Do NOT introduce a parallel `{ success, shouldContinue, output }` envelope. +- Tools that touch a fiscal-period-bound date (categorize, mark paid, create voucher, correct/reverse entry, approve supplier invoice) pass `dateForPeriodCheck` to `stagePendingOperation` so the response includes `period_status: { period_id, status: open|locked|closed, lock_date }`. Widgets and agents use this to disable writes without round-trips. diff --git a/.claude/skills/create-extension/SKILL.md b/.claude/skills/create-extension/SKILL.md index 40130aff..e1558c54 100644 --- a/.claude/skills/create-extension/SKILL.md +++ b/.claude/skills/create-extension/SKILL.md @@ -1,6 +1,6 @@ --- name: create-extension -description: "Generate and implement extensions for gnubok: scaffold files, configure manifests, write event handlers, API routes, services, workspace UIs, settings panels, and testing. Use when creating new extensions, adding surfaces to existing extensions, or understanding the extension architecture. Covers the full lifecycle from scaffolding to registration." +description: "Generate and implement extensions for Accounted: scaffold files, configure manifests, write event handlers, API routes, services, workspace UIs, settings panels, and testing. Use when creating new extensions, adding surfaces to existing extensions, or understanding the extension architecture. Covers the full lifecycle from scaffolding to registration." --- # Extension Generator diff --git a/.claude/skills/create-extension/references/workspace-ui.md b/.claude/skills/create-extension/references/workspace-ui.md index f7da90c2..4b72398a 100644 --- a/.claude/skills/create-extension/references/workspace-ui.md +++ b/.claude/skills/create-extension/references/workspace-ui.md @@ -59,5 +59,4 @@ const isAvailable = ENABLED_EXTENSION_IDS.has('my-extension') ```typescript const { data, save, remove, getByKey } = useExtensionData('general', 'my-extension') -const { totals, monthly, totalNet } = useAccountTotals({ from, to, monthly: true }) ``` diff --git a/.claude/skills/create-ticket/prompts/bookkeeping.md b/.claude/skills/create-ticket/prompts/bookkeeping.md index 267ca3af..0791e016 100644 --- a/.claude/skills/create-ticket/prompts/bookkeeping.md +++ b/.claude/skills/create-ticket/prompts/bookkeeping.md @@ -2,7 +2,7 @@ ## Perspective -You are scanning for Swedish accounting compliance issues, bookkeeping logic gaps, and financial data handling problems. gnubok implements double-entry bookkeeping compliant with Bokforingslagen (BFL) and BFN standards. Focus on correctness of journal entries, VAT handling, account mappings, and legal guardrails. +You are scanning for Swedish accounting compliance issues, bookkeeping logic gaps, and financial data handling problems. Accounted implements double-entry bookkeeping compliant with Bokforingslagen (BFL) and BFN standards. Focus on correctness of journal entries, VAT handling, account mappings, and legal guardrails. ## Checklist diff --git a/.claude/skills/create-ticket/prompts/design.md b/.claude/skills/create-ticket/prompts/design.md index a995592a..8a19bcc4 100644 --- a/.claude/skills/create-ticket/prompts/design.md +++ b/.claude/skills/create-ticket/prompts/design.md @@ -2,18 +2,18 @@ ## Perspective -You are scanning for design and UX issues in the gnubok interface. The app follows a minimal, sharp, efficient aesthetic inspired by Mercury (banking). Evaluate against the gnubok design system: grayscale palette with sage green/terracotta/ochre accents, Fraunces headings, Geist body, generous whitespace, subtle motion. +You are scanning for design and UX issues in the Accounted interface. The app follows an editorial-monochrome aesthetic — paper-white surfaces, hairline borders, serif headlines; considered and quiet. Evaluate against the Accounted design system: achromatic palette (semantic sage/ochre/terracotta are data-only — charts and financial deltas, never chrome), Hedvig Letters Serif display headings, Geist body, generous whitespace, functional (non-decorative) motion. ## Checklist ### Consistency & Design System - [ ] Spacing values use Tailwind scale (not arbitrary values) - [ ] Colors are from the design palette (grayscale, sage green, terracotta, ochre) -- [ ] Fraunces used for display headings, Geist for body text +- [ ] Hedvig Letters Serif used for display headings, Geist for body text - [ ] `tabular-nums` applied to all financial/numeric data - [ ] shadcn/ui components used where appropriate - [ ] Icon sizes consistent (15px nav, larger for empty states) -- [ ] Border styles consistent (subtle, 60% opacity) +- [ ] Borders are full-opacity `border-border` on cards/surfaces (no `border-border/60`) ### Loading & Empty States - [ ] Pages have loading states (skeletons preferred over spinners) @@ -28,7 +28,7 @@ You are scanning for design and UX issues in the gnubok interface. The app follo ### Animation & Motion - [ ] List items stagger-animate on entry - [ ] Interactive elements have hover/active transitions -- [ ] Transitions use appropriate easing (spring for feedback, ease for reveals) +- [ ] Transitions use the project default (`transition-colors duration-150`) — no spring/overshoot - [ ] `prefers-reduced-motion` respected - [ ] No abrupt state changes that need transitions diff --git a/.claude/skills/erp-api-route/SKILL.md b/.claude/skills/erp-api-route/SKILL.md index cc7de45e..bb7e3761 100644 --- a/.claude/skills/erp-api-route/SKILL.md +++ b/.claude/skills/erp-api-route/SKILL.md @@ -1,6 +1,6 @@ --- name: erp-api-route -description: "Generate Next.js 16 API routes for gnubok with correct auth guards, Supabase client usage, event emission, journal entry creation, and error handling. Use when creating new API endpoints in app/api/. Handles the Next.js 16 async params pattern, ensureInitialized() for events, non-blocking journal entry wrapping, and defense-in-depth user_id filtering." +description: "Generate Next.js 16 API routes for Accounted with correct auth guards, Supabase client usage, event emission, journal entry creation, and error handling. Use when creating new API endpoints in app/api/. Handles the Next.js 16 async params pattern, ensureInitialized() for events, non-blocking journal entry wrapping, and defense-in-depth user_id filtering." --- # ERP API Route Generator diff --git a/.claude/skills/mcp-builder/SKILL.md b/.claude/skills/mcp-builder/SKILL.md index 8a1a77a4..a8235957 100644 --- a/.claude/skills/mcp-builder/SKILL.md +++ b/.claude/skills/mcp-builder/SKILL.md @@ -1,7 +1,6 @@ --- name: mcp-builder description: Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK). -license: Complete terms in LICENSE.txt --- # MCP Server Development Guide diff --git a/.claude/skills/modifier/single-shareholder-ab-fmb/SKILL.md b/.claude/skills/modifier/single-shareholder-ab-fmb/SKILL.md index e301a952..c3bff977 100644 --- a/.claude/skills/modifier/single-shareholder-ab-fmb/SKILL.md +++ b/.claude/skills/modifier/single-shareholder-ab-fmb/SKILL.md @@ -23,7 +23,7 @@ version: 1 ## When this applies The active company is an aktiebolag where one natural person owns > 50% of -the shares. This is the most common form among Gnubok's AB users. The owner +the shares. This is the most common form among Accounted's AB users. The owner is typically also a full-time anställd in the company and "verksam i betydande omfattning" — which triggers 3:12-reglerna (53 kap. IL). diff --git a/.claude/skills/scout-design/SKILL.md b/.claude/skills/scout-design/SKILL.md index 666215f6..94d3cc53 100644 --- a/.claude/skills/scout-design/SKILL.md +++ b/.claude/skills/scout-design/SKILL.md @@ -1,6 +1,6 @@ --- name: scout-design -description: "Scan a specific area of the app for design issues and improvement opportunities, then create Linear tickets for approved findings. Usage: /scout-design (e.g., /scout-design settings, /scout-design bookkeeping). Evaluates against the gnubok design system for consistency, missing states, animation gaps, accessibility, and visual polish." +description: "Scan a specific area of the app for design issues and improvement opportunities, then create Linear tickets for approved findings. Usage: /scout-design (e.g., /scout-design settings, /scout-design bookkeeping). Evaluates against the Accounted design system for consistency, missing states, animation gaps, accessibility, and visual polish." --- # Scout Design @@ -31,11 +31,11 @@ Read ALL files in the resolved scope (pages + components). For each file, evalua **Consistency & Design System** - Are spacing values consistent (using Tailwind scale, not arbitrary values)? - Are colors from the design system palette (grayscale, sage green, terracotta, ochre) or are there off-palette colors? -- Are font sizes/weights consistent with the typography system (Fraunces for headings, Geist for body)? +- Are font sizes/weights consistent with the typography system (Hedvig Letters Serif for display headings, Geist for body)? - Are `tabular-nums` applied to all financial/numeric data? - Are shadcn/ui components used where appropriate, or are there custom implementations that should use shadcn? - Are icon sizes consistent (15px nav, larger for empty states)? -- Are border styles consistent (subtle, 60% opacity)? +- Are borders full-opacity `border-border` on cards/surfaces (no opacity-suffixed border classes like `border-border/60`)? **Loading & Empty States** - Does the page/component have a loading state? (skeleton, spinner, or shimmer) @@ -50,7 +50,7 @@ Read ALL files in the resolved scope (pages + components). For each file, evalua **Animation & Motion** - Are list items stagger-animated on entry? - Do interactive elements have hover/active transitions? -- Are transitions using appropriate easing (spring for feedback, ease for reveals)? +- Are transitions using the project default (`transition-colors duration-150`) without spring/overshoot? - Is `prefers-reduced-motion` respected? - Are there abrupt state changes that would benefit from a transition? diff --git a/.claude/skills/supabase-migration/SKILL.md b/.claude/skills/supabase-migration/SKILL.md index ce6c67b3..489d36aa 100644 --- a/.claude/skills/supabase-migration/SKILL.md +++ b/.claude/skills/supabase-migration/SKILL.md @@ -1,22 +1,23 @@ --- name: supabase-migration -description: "Generate Supabase database migrations for the gnubok project with correct RLS policies, triggers, indexes, and Swedish accounting constraints. Use when creating new tables, adding columns, modifying constraints (e.g. source_type CHECK), or any DDL operation on the Supabase database. Ensures legal compliance with BFL 7-year retention, immutability triggers, and period lock enforcement." +description: "Generate Supabase database migrations for the Accounted project with correct RLS policies, triggers, indexes, and Swedish accounting constraints. Use when creating new tables, adding columns, modifying constraints (e.g. source_type CHECK), or any DDL operation on the Supabase database. Ensures legal compliance with BFL 7-year retention, immutability triggers, and period lock enforcement." --- # Supabase Migration Generator ## Migration Numbering -Series: `20240101000001` through `20240101000028`. Next: `20240101000029`. Increment from there. +Early migrations used the sequential series `20240101000001`–`20240101000038`; the project has long since moved to real timestamps. **New migrations use a current UTC timestamp** `YYYYMMDDHHMMSS_description.sql` (e.g. `20260603120000_add_x.sql`). Never reuse or back-date a number. ## New Table — Complete Template -Every new table requires ALL five parts. Missing any is a bug. +Accounted is multi-tenant: company-scoped business data is owned by `company_id` and secured with the `user_company_ids()` RLS helper (NOT `auth.uid() = user_id`). Every new company-scoped table requires ALL of these. Missing any is a bug. ```sql --- 1. Table with UUID PK + user_id FK +-- 1. Table with UUID PK + company_id + user_id FKs CREATE TABLE public.tablename ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + company_id uuid NOT NULL REFERENCES public.companies(id) ON DELETE CASCADE, user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, -- domain columns -- created_at timestamptz NOT NULL DEFAULT now(), @@ -26,18 +27,18 @@ CREATE TABLE public.tablename ( -- 2. RLS ALTER TABLE public.tablename ENABLE ROW LEVEL SECURITY; --- 3. All four CRUD policies -CREATE POLICY "Users can view own tablename" - ON public.tablename FOR SELECT USING (auth.uid() = user_id); -CREATE POLICY "Users can insert own tablename" - ON public.tablename FOR INSERT WITH CHECK (auth.uid() = user_id); -CREATE POLICY "Users can update own tablename" - ON public.tablename FOR UPDATE USING (auth.uid() = user_id); -CREATE POLICY "Users can delete own tablename" - ON public.tablename FOR DELETE USING (auth.uid() = user_id); +-- 3. All four CRUD policies — scope to the user's companies +CREATE POLICY "view own-company tablename" + ON public.tablename FOR SELECT USING (company_id IN (SELECT user_company_ids())); +CREATE POLICY "insert own-company tablename" + ON public.tablename FOR INSERT WITH CHECK (company_id IN (SELECT user_company_ids())); +CREATE POLICY "update own-company tablename" + ON public.tablename FOR UPDATE USING (company_id IN (SELECT user_company_ids())); +CREATE POLICY "delete own-company tablename" + ON public.tablename FOR DELETE USING (company_id IN (SELECT user_company_ids())); --- 4. Indexes (minimum: user_id + any FK/filter columns) -CREATE INDEX idx_tablename_user_id ON public.tablename (user_id); +-- 4. Indexes (minimum: company_id + any FK/filter columns) +CREATE INDEX idx_tablename_company_id ON public.tablename (company_id); -- 5. updated_at trigger CREATE TRIGGER set_updated_at_tablename @@ -48,6 +49,9 @@ CREATE TRIGGER set_updated_at_tablename CREATE TRIGGER audit_tablename AFTER INSERT OR UPDATE OR DELETE ON public.tablename FOR EACH ROW EXECUTE FUNCTION public.write_audit_log(); + +-- 7. Reload PostgREST schema cache (required after structural DDL) +NOTIFY pgrst, 'reload schema'; ``` ## Child Tables (No Direct user_id) @@ -55,11 +59,12 @@ CREATE TRIGGER audit_tablename Tables owned via parent use subquery-based RLS: ```sql -CREATE POLICY "Users can view own child" ON public.child_table +CREATE POLICY "view own-company child" ON public.child_table FOR SELECT USING ( EXISTS ( SELECT 1 FROM public.parent_table pt - WHERE pt.id = child_table.parent_id AND pt.user_id = auth.uid() + WHERE pt.id = child_table.parent_id + AND pt.company_id IN (SELECT user_company_ids()) ) ); -- Repeat for INSERT (WITH CHECK), UPDATE, DELETE diff --git a/.claude/skills/supportmail-to-ticket/SKILL.md b/.claude/skills/supportmail-to-ticket/SKILL.md index cdd034b4..25656242 100644 --- a/.claude/skills/supportmail-to-ticket/SKILL.md +++ b/.claude/skills/supportmail-to-ticket/SKILL.md @@ -1,11 +1,11 @@ --- name: supportmail-to-ticket -description: "Triage Gnubok customer support emails and turn them into GitHub issues in the erp-mafia/gnubok repo. Use this skill whenever the user invokes /supportmail-to-ticket (with or without a number argument), or asks to 'triage support mail', 'turn support emails into tickets', 'process gnubok support', 'check the support inbox and file issues', or any similar phrasing involving the Gnubok support mailbox. Also trigger this skill if the user mentions [gnubok support] emails and wants them converted into actionable work — even if they don't use the exact slash command." +description: "Triage Accounted customer support emails and turn them into GitHub issues in the erp-mafia/gnubok repo. Use this skill whenever the user invokes /supportmail-to-ticket (with or without a number argument), or asks to 'triage support mail', 'turn support emails into tickets', 'process Accounted support', 'check the support inbox and file issues', or any similar phrasing involving the Accounted support mailbox. Also trigger this skill if the user mentions [Accounted support] emails and wants them converted into actionable work — even if they don't use the exact slash command." --- # supportmail-to-ticket -Triage `[gnubok support]` emails from Gmail, cross-reference them against the local erp-base codebase, and draft GitHub issues for the `erp-mafia/gnubok` repo — with inline user approval before anything gets created. +Triage `[Accounted support]` emails from Gmail, cross-reference them against the local erp-base codebase, and draft GitHub issues for the `erp-mafia/gnubok` repo — with inline user approval before anything gets created. ## Invocation @@ -15,7 +15,7 @@ Primary form: /supportmail-to-ticket [N] ``` -- `N` = number of most recent `[gnubok support]` threads to triage. Optional. Default: `3`. +- `N` = number of most recent `[Accounted support]` threads to triage. Optional. Default: `3`. - Examples: `/supportmail-to-ticket`, `/supportmail-to-ticket 10`, `/supportmail-to-ticket 1` If the user phrases the request in natural language ("triage the last 5 support mails", "check the inbox"), extract the number if present, otherwise use `3`. @@ -36,7 +36,7 @@ Follow these five phases in order. Do not skip phase 4 (approval). Call Gmail `search_threads` with: -- `query`: `subject:"[gnubok support]"` +- `query`: `subject:"[Accounted support]"` - `pageSize`: the requested N (default 3) For each returned thread, call `get_thread` with `messageFormat: FULL_CONTENT` to retrieve the full body. Extract per thread: @@ -113,7 +113,7 @@ The goal: strip anything that identifies the **specific customer or their employ - **Replace personal first and last names with `x`**. Example: *"Hey this is amazing. My name is Emil and I do bla bla"* → *"Hey this is amazing. My name is x and I do bla bla"*. Handles greetings (*"Hej Anna,"* → *"Hej x,"*) and signatures (*"/Lars Andersson"* → *"/x"*). - **Replace the customer's employer / own company name with `x`**, wherever it appears — body text, signatures, "jag jobbar på …", "vi på …", org numbers attributed to the sender, `@company.com` email domains. Also redact names of their direct clients or other companies they identify themselves through. Example: *"Jag jobbar på Capnos och behöver ta bort Capnos"* → *"Jag jobbar på x och behöver ta bort x"*. - **Do NOT redact** (these are not identifying — they're context): - - **gnubok itself** + - **Accounted itself** - **Swedish authorities / standard bodies**: Skatteverket, Bolagsverket, Försäkringskassan, Bankgirot, BFN - **Accounting / ERP providers and competitors**: Fortnox, Visma, Bokio, SpeedLedger, BL/Björn Lundén, Briox, etc. - **Banks by name**: Swedbank, SEB, Handelsbanken, Nordea, etc. (unless clearly the customer's *own* company — rare) @@ -221,7 +221,7 @@ gh issue comment \ - If `gh issue create` fails with an error mentioning an unknown label (exit code non-zero, stderr contains `"could not add label"` or `"not found"`), retry the command without that `--label` flag and tell the user which labels are missing so they can create them manually — do **not** attempt to create labels automatically. - You can check available labels once at the start of phase 5 with: `gh label list --repo erp-mafia/gnubok --limit 100 --json name` — useful if multiple label errors happen in a row. -**Project board**: Issues are created in the `erp-mafia/gnubok` repo. The Gnubok project board (`erp-mafia/projects/...`) aggregates issues but adding to a project via `gh` requires `gh project item-add` with the project number and GraphQL scopes that may not be in the current auth token. After creating issues, output the project URL once and remind the user they may want to drag the new issues onto the board. Example wording: *"Issues created. If you want them on the Gnubok project board, you'll need to add them manually at https://github.com/orgs/erp-mafia/projects — or run `gh project item-add` if you have project scopes on your token."* +**Project board**: Issues are created in the `erp-mafia/gnubok` repo. The Accounted project board (`erp-mafia/projects/...`) aggregates issues but adding to a project via `gh` requires `gh project item-add` with the project number and GraphQL scopes that may not be in the current auth token. After creating issues, output the project URL once and remind the user they may want to drag the new issues onto the board. Example wording: *"Issues created. If you want them on the Accounted project board, you'll need to add them manually at https://github.com/orgs/erp-mafia/projects — or run `gh project item-add` if you have project scopes on your token."* ### Phase 6 — Summary diff --git a/.claude/skills/swarm-a11y-agent/SKILL.md b/.claude/skills/swarm-a11y-agent/SKILL.md deleted file mode 100644 index a587f958..00000000 --- a/.claude/skills/swarm-a11y-agent/SKILL.md +++ /dev/null @@ -1,177 +0,0 @@ ---- -name: swarm-a11y-agent -description: "Read-only accessibility audit agent for gnubok. Sweeps for WCAG AA violations: text contrast (4.5:1), UI contrast (3:1), keyboard navigation, visible focus rings, aria-labels on icon-only buttons, semantic HTML, form label association, color-only indicators, motion respect for prefers-reduced-motion, screen reader support. Invoked by /swarm — not for direct user use." ---- - -# swarm-a11y-agent - -You are a read-only accessibility audit agent. Your lens is **WCAG AA compliance**. You never write code, never create tickets, never commit. - -## Baseline (from `CLAUDE.md` § Accessibility) - -- **WCAG AA**: 4.5:1 text contrast, 3:1 UI contrast -- Keyboard-navigable with visible focus rings -- Respect `prefers-reduced-motion` -- Color never the sole indicator of state — pair with icon, text, or shape - -Users: Swedish professionals using gnubok in short, focused sessions. Keyboard use is common for power users (tab through forms rapidly). Screen reader users are fewer but a compliance requirement. - -## Files to sweep - -- `app/**/*.tsx` and `app/**/*.jsx` — pages, layouts -- `components/**/*.tsx` — reusable UI -- `app/globals.css`, Tailwind config — focus ring styles, color tokens - -Skip: `node_modules/`, `.next/`, `.swarm/`, `packages/gnubok-mcp/dist/`, `lib/extensions/_generated/`, `app/api/**`. - -## What to look for - -### Keyboard navigation -- Every interactive element is focusable via Tab (no `tabIndex={-1}` on primary actions) -- Tab order matches visual order (no weird jumps due to CSS positioning) -- Custom components handle keyboard properly: - - Custom `` has an associated `