feat: production readiness — 3-extension deploy with security hardening and observability

- Strip extensions to enable-banking, ai-categorization, ai-chat only
- Remove push-notifications cron from vercel.json
- Add security headers (HSTS, CSP, X-Frame-Options, Permissions-Policy)
- Add /api/health endpoint for uptime monitoring
- Add env var validation in ensureInitialized()
- Fix SIE4 #IB opening balance records from year-end closing entry
- Replace in-memory ai-chat rate limiter with Supabase-backed distributed rate limiting
- Add Sentry error tracking scaffolding (@sentry/nextjs, instrumentation hook)
- Add AI token usage tracking (migration 047, usage-tracker, wired into both AI extensions)
- Include pending enable-banking and dashboard improvements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-03-02 11:32:43 +01:00
co-authored by Claude Opus 4.6
parent 1a1c6ba40f
commit 13725ffc16
33 changed files with 2852 additions and 271 deletions
@@ -14,6 +14,7 @@ import 'server-only'
import Anthropic from '@anthropic-ai/sdk'
import { BOOKING_TEMPLATES, type BookingTemplate } from '@/lib/bookkeeping/booking-templates'
import type { TransactionCategory, EntityType } from '@/types'
import type { SupabaseClient } from '@supabase/supabase-js'
// ============================================================
// Types
@@ -72,10 +73,16 @@ export interface CategorizationSuggestion {
templateId?: string
}
export interface TrackingContext {
supabase: SupabaseClient
userId: string
}
export interface CategorizationProvider {
categorize(
transactions: TransactionForCategorization[],
context: CategorizationContext | EnrichedCategorizationContext
context: CategorizationContext | EnrichedCategorizationContext,
tracking?: TrackingContext
): Promise<CategorizationSuggestion[]>
}
@@ -198,7 +205,8 @@ export class AnthropicCategorizationProvider implements CategorizationProvider {
async categorize(
transactions: TransactionForCategorization[],
context: CategorizationContext | EnrichedCategorizationContext
context: CategorizationContext | EnrichedCategorizationContext,
tracking?: TrackingContext
): Promise<CategorizationSuggestion[]> {
// Cap batch size
const batch = transactions.slice(0, MAX_BATCH_SIZE)
@@ -318,6 +326,16 @@ ${transactionList}`
],
})
// Track token usage (fire-and-forget)
if (tracking && message.usage) {
const { trackTokenUsage } = await import('@/lib/ai/usage-tracker')
trackTokenUsage(tracking.supabase, tracking.userId, 'ai-categorization', {
inputTokens: message.usage.input_tokens,
outputTokens: message.usage.output_tokens,
model: this.model,
})
}
// Extract tool_use block from response
const toolUseBlock = message.content.find(
(block) => block.type === 'tool_use' && block.name === 'classify_transactions'
@@ -10,6 +10,7 @@ import {
type CategorizationSuggestion,
type AccountUsageEntry,
type MerchantHistoryEntry,
type TrackingContext,
} from './categorizer'
import type { BookingTemplate } from '@/lib/bookkeeping/booking-templates'
@@ -124,7 +125,7 @@ export async function categorizeTransactions(
const context = await buildEnrichedContext(userId, supabase, batch)
const aiProvider = getProvider(settings.providerModel)
const suggestions = await aiProvider.categorize(batch, context)
const suggestions = await aiProvider.categorize(batch, context, { supabase, userId })
// Store suggestions
await storeSuggestions(userId, suggestions, supabase)
@@ -174,7 +175,7 @@ async function handleTransactionSynced(
const context = await buildEnrichedContext(userId, supabase, batch)
const aiProvider = getProvider(settings.providerModel)
const suggestions = await aiProvider.categorize(batch, context)
const suggestions = await aiProvider.categorize(batch, context, { supabase, userId })
// Store only suggestions above confidence threshold
const qualifiedSuggestions = suggestions.filter(