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:
co-authored by
Claude Opus 4.6
parent
1a1c6ba40f
commit
13725ffc16
@@ -0,0 +1,35 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { createLogger } from '@/lib/logger'
|
||||
|
||||
const log = createLogger('ai-usage')
|
||||
|
||||
interface TokenUsage {
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
model: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Track AI token usage. Non-blocking — errors are logged, not thrown.
|
||||
*/
|
||||
export function trackTokenUsage(
|
||||
supabase: SupabaseClient,
|
||||
userId: string,
|
||||
extensionId: string,
|
||||
usage: TokenUsage
|
||||
): void {
|
||||
supabase
|
||||
.from('ai_usage_tracking')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
extension_id: extensionId,
|
||||
model: usage.model,
|
||||
input_tokens: usage.inputTokens,
|
||||
output_tokens: usage.outputTokens,
|
||||
})
|
||||
.then(({ error }) => {
|
||||
if (error) {
|
||||
log.error(`Failed to track usage for ${extensionId}:`, error.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,12 +1,7 @@
|
||||
// AUTO-GENERATED — do not edit. Run `npm run setup:extensions` to regenerate.
|
||||
|
||||
export const ENABLED_EXTENSION_IDS: ReadonlySet<string> = new Set([
|
||||
'receipt-ocr',
|
||||
'enable-banking',
|
||||
'ai-categorization',
|
||||
'ai-chat',
|
||||
'push-notifications',
|
||||
'invoice-inbox',
|
||||
'calendar',
|
||||
'enable-banking',
|
||||
'email',
|
||||
])
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
// AUTO-GENERATED — do not edit. Run `npm run setup:extensions` to regenerate.
|
||||
import type { Extension } from '../types'
|
||||
import { receiptOcrExtension } from '@/extensions/general/receipt-ocr'
|
||||
import { enableBankingExtension } from '@/extensions/general/enable-banking'
|
||||
import { aiCategorizationExtension } from '@/extensions/general/ai-categorization'
|
||||
import { aiChatExtension } from '@/extensions/general/ai-chat'
|
||||
import { pushNotificationsExtension } from '@/extensions/general/push-notifications'
|
||||
import { invoiceInboxExtension } from '@/extensions/general/invoice-inbox'
|
||||
import { calendarExtension } from '@/extensions/general/calendar'
|
||||
import { enableBankingExtension } from '@/extensions/general/enable-banking'
|
||||
import { emailExtension } from '@/extensions/general/email'
|
||||
|
||||
export const FIRST_PARTY_EXTENSIONS: Extension[] = [
|
||||
receiptOcrExtension,
|
||||
enableBankingExtension,
|
||||
aiCategorizationExtension,
|
||||
aiChatExtension,
|
||||
pushNotificationsExtension,
|
||||
invoiceInboxExtension,
|
||||
calendarExtension,
|
||||
enableBankingExtension,
|
||||
emailExtension,
|
||||
]
|
||||
|
||||
@@ -4,21 +4,16 @@ import type { ExtensionDefinition } from '../types'
|
||||
export const EXTENSION_DEFINITIONS: Record<string, ExtensionDefinition[]> = {
|
||||
'general': [
|
||||
{
|
||||
"slug": "receipt-ocr",
|
||||
"name": "Kvittoscanning",
|
||||
"slug": "enable-banking",
|
||||
"name": "Bankintegration (PSD2)",
|
||||
"sector": "general",
|
||||
"category": "import",
|
||||
"icon": "Camera",
|
||||
"icon": "Landmark",
|
||||
"dataPattern": "manual",
|
||||
"description": "Skanna kvitton och extrahera data automatiskt",
|
||||
"longDescription": "Ladda upp kvittofoton och låt systemet automatiskt extrahera leverantör, belopp, moms och datum. Sparar tid och minskar manuell inmatning.",
|
||||
"description": "Automatisk banktransaktionssynk via PSD2",
|
||||
"longDescription": "Koppla ditt bankkonto direkt och synka transaktioner automatiskt via säker PSD2-bankintegration. Stöder de flesta svenska banker.",
|
||||
"hasOwnData": true,
|
||||
"quickAction": {
|
||||
"label": "Skanna kvitto",
|
||||
"description": "Fotografera & spara",
|
||||
"icon": "Camera",
|
||||
"href": "/receipts/scan"
|
||||
}
|
||||
"subscriptionNotice": "Denna integration kräver ett aktivt Enable Banking-abonnemang. Utan abonnemang kommer bankintegration inte att fungera."
|
||||
},
|
||||
{
|
||||
"slug": "ai-categorization",
|
||||
@@ -62,79 +57,5 @@ export const EXTENSION_DEFINITIONS: Record<string, ExtensionDefinition[]> = {
|
||||
"event": "open-ai-chat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"slug": "push-notifications",
|
||||
"name": "Push-notiser",
|
||||
"sector": "general",
|
||||
"category": "operations",
|
||||
"icon": "Bell",
|
||||
"dataPattern": "core",
|
||||
"description": "Händelsenotiser för bokföringsaktiviteter",
|
||||
"longDescription": "Få push-notiser direkt i webbläsaren när viktiga händelser sker — nya fakturor, förfallna betalningar, slutförda bokföringar med mera.",
|
||||
"readsCoreTables": [
|
||||
"journal_entries",
|
||||
"invoices",
|
||||
"receipts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "invoice-inbox",
|
||||
"name": "Dokumentinkorg",
|
||||
"sector": "general",
|
||||
"category": "import",
|
||||
"icon": "Inbox",
|
||||
"dataPattern": "manual",
|
||||
"description": "Ta emot alla dokument via e-post — fakturor, kvitton och myndighetspost",
|
||||
"longDescription": "Skicka alla affärsdokument till en dedikerad e-postadress. AI klassificerar automatiskt dokumenttyp (faktura, kvitto, myndighetspost), extraherar data och matchar mot transaktioner. En inkorg för alla dokument.",
|
||||
"hasOwnData": true,
|
||||
"quickAction": {
|
||||
"label": "Dokumentinkorg",
|
||||
"description": "Granska inkommande dokument",
|
||||
"icon": "Inbox",
|
||||
"href": "/e/general/invoice-inbox"
|
||||
}
|
||||
},
|
||||
{
|
||||
"slug": "calendar",
|
||||
"name": "Kalender",
|
||||
"sector": "general",
|
||||
"category": "operations",
|
||||
"icon": "Calendar",
|
||||
"dataPattern": "core",
|
||||
"description": "Fullständig kalendervy med månads-, vecko- och dagsvisning",
|
||||
"longDescription": "Se alla fakturadatum och deadlines i en interaktiv kalender med månads-, vecko- och dagsvy.",
|
||||
"readsCoreTables": [
|
||||
"invoices",
|
||||
"deadlines",
|
||||
"customers"
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "enable-banking",
|
||||
"name": "Bankintegration (PSD2)",
|
||||
"sector": "general",
|
||||
"category": "import",
|
||||
"icon": "Landmark",
|
||||
"dataPattern": "manual",
|
||||
"description": "Automatisk banktransaktionssynk via PSD2",
|
||||
"longDescription": "Koppla ditt bankkonto direkt och synka transaktioner automatiskt via säker PSD2-bankintegration. Stöder de flesta svenska banker.",
|
||||
"hasOwnData": true,
|
||||
"subscriptionNotice": "Denna integration kräver ett aktivt Enable Banking-abonnemang. Utan abonnemang kommer bankintegration inte att fungera."
|
||||
},
|
||||
{
|
||||
"slug": "email",
|
||||
"name": "E-post (Resend)",
|
||||
"sector": "general",
|
||||
"category": "operations",
|
||||
"icon": "Mail",
|
||||
"dataPattern": "core",
|
||||
"description": "Skicka fakturor och påminnelser via e-post",
|
||||
"longDescription": "Aktiverar e-postfunktioner: skicka fakturor till kunder, automatiska betalningspåminnelser (15/30/45 dagar), och e-postmeddelanden. Kräver ett Resend-konto med verifierad domän.",
|
||||
"readsCoreTables": [
|
||||
"invoices",
|
||||
"customers",
|
||||
"company_settings"
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -4,11 +4,7 @@ import type { ComponentType } from 'react'
|
||||
import type { WorkspaceComponentProps } from '../workspace-registry'
|
||||
|
||||
export const WORKSPACES: Record<string, ComponentType<WorkspaceComponentProps>> = {
|
||||
'general/receipt-ocr': dynamic(() => import('@/components/extensions/general/ReceiptOcrWorkspace')),
|
||||
'general/enable-banking': dynamic(() => import('@/components/extensions/general/EnableBankingWorkspace')),
|
||||
'general/ai-categorization': dynamic(() => import('@/components/extensions/general/AiCategorizationWorkspace')),
|
||||
'general/ai-chat': dynamic(() => import('@/components/extensions/general/AiChatWorkspace')),
|
||||
'general/push-notifications': dynamic(() => import('@/components/extensions/general/PushNotificationsWorkspace')),
|
||||
'general/invoice-inbox': dynamic(() => import('@/components/extensions/general/DocumentInboxWorkspace')),
|
||||
'general/calendar': dynamic(() => import('@/components/extensions/general/CalendarWorkspace')),
|
||||
'general/enable-banking': dynamic(() => import('@/components/extensions/general/EnableBankingWorkspace')),
|
||||
}
|
||||
|
||||
+52
@@ -2,9 +2,60 @@ import { loadExtensions } from '@/lib/extensions/loader'
|
||||
import { setContextFactory } from '@/lib/extensions/registry'
|
||||
import { createExtensionContext } from '@/lib/extensions/context-factory'
|
||||
import { registerSupplierInvoiceHandler } from '@/lib/bookkeeping/handlers/supplier-invoice-handler'
|
||||
import { createLogger } from '@/lib/logger'
|
||||
|
||||
const log = createLogger('init')
|
||||
|
||||
let initialized = false
|
||||
|
||||
const REQUIRED_CORE_VARS = [
|
||||
'NEXT_PUBLIC_SUPABASE_URL',
|
||||
'NEXT_PUBLIC_SUPABASE_ANON_KEY',
|
||||
'SUPABASE_SERVICE_ROLE_KEY',
|
||||
'NEXT_PUBLIC_APP_URL',
|
||||
'CRON_SECRET',
|
||||
] as const
|
||||
|
||||
const REQUIRED_EXTENSION_VARS = [
|
||||
'ENABLE_BANKING_APP_ID',
|
||||
'ENABLE_BANKING_PRIVATE_KEY',
|
||||
'ANTHROPIC_API_KEY',
|
||||
'OPENAI_API_KEY',
|
||||
] as const
|
||||
|
||||
const OPTIONAL_VARS = [
|
||||
'SENTRY_DSN',
|
||||
'LANGFUSE_SECRET_KEY',
|
||||
'LANGFUSE_PUBLIC_KEY',
|
||||
] as const
|
||||
|
||||
function validateEnvironment(): void {
|
||||
const missing: string[] = []
|
||||
|
||||
for (const v of REQUIRED_CORE_VARS) {
|
||||
if (!process.env[v]) missing.push(v)
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`Missing required environment variables: ${missing.join(', ')}`)
|
||||
}
|
||||
|
||||
const missingExt: string[] = []
|
||||
for (const v of REQUIRED_EXTENSION_VARS) {
|
||||
if (!process.env[v]) missingExt.push(v)
|
||||
}
|
||||
|
||||
if (missingExt.length > 0) {
|
||||
throw new Error(`Missing required extension environment variables: ${missingExt.join(', ')}`)
|
||||
}
|
||||
|
||||
for (const v of OPTIONAL_VARS) {
|
||||
if (!process.env[v]) {
|
||||
log.warn(`Optional environment variable ${v} is not set`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the system is initialized (extensions loaded, context factory wired,
|
||||
* core event handlers registered).
|
||||
@@ -15,6 +66,7 @@ export function ensureInitialized(): void {
|
||||
if (initialized) return
|
||||
initialized = true
|
||||
|
||||
validateEnvironment()
|
||||
setContextFactory(createExtensionContext)
|
||||
registerSupplierInvoiceHandler()
|
||||
loadExtensions()
|
||||
|
||||
@@ -116,8 +116,21 @@ export async function generateSIEExport(
|
||||
}
|
||||
|
||||
// === Opening balances (IB) ===
|
||||
// For now, all zeros unless we have data from previous periods
|
||||
// #IB 0 accountNumber amount
|
||||
if (period.opening_balance_entry_id) {
|
||||
const { data: obEntry } = await supabase
|
||||
.from('journal_entries')
|
||||
.select('*, lines:journal_entry_lines(*)')
|
||||
.eq('id', period.opening_balance_entry_id)
|
||||
.eq('user_id', userId)
|
||||
.single()
|
||||
|
||||
if (obEntry?.lines) {
|
||||
for (const line of (obEntry.lines as JournalEntryLine[])) {
|
||||
const amount = (Number(line.debit_amount) || 0) - (Number(line.credit_amount) || 0)
|
||||
lines.push(`#IB 0 ${line.account_number} ${formatAmount(amount)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Journal entries (VER + TRANS) ===
|
||||
for (const entry of (entries as JournalEntry[]) || []) {
|
||||
|
||||
Reference in New Issue
Block a user