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
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { formatDate } from '@/lib/utils'
|
||||
import { getDaysUntilExpiry, isConsentExpiringSoon } from '../lib/api-client'
|
||||
@@ -67,8 +68,19 @@ export function BankConnectionStatus({
|
||||
name?: string
|
||||
currency: string
|
||||
balance?: number
|
||||
balance_updated_at?: string
|
||||
}>) || []
|
||||
|
||||
const [now] = useState(() => Date.now())
|
||||
|
||||
function formatBalanceAge(updatedAt: string): string {
|
||||
const hoursAgo = Math.floor((now - new Date(updatedAt).getTime()) / (1000 * 60 * 60))
|
||||
if (hoursAgo < 1) return 'Nyss uppdaterat'
|
||||
if (hoursAgo < 24) return `${hoursAgo}h sedan`
|
||||
const daysAgo = Math.floor(hoursAgo / 24)
|
||||
return `${daysAgo}d sedan`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg p-4 space-y-4">
|
||||
{/* Header */}
|
||||
@@ -155,6 +167,11 @@ export function BankConnectionStatus({
|
||||
currency: account.currency,
|
||||
}).format(account.balance)}
|
||||
</p>
|
||||
{account.balance_updated_at && (
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
{formatBalanceAge(account.balance_updated_at)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { DestructiveConfirmDialog, useDestructiveConfirm } from '@/components/ui/destructive-confirm-dialog'
|
||||
import { Loader2, Landmark } from 'lucide-react'
|
||||
import { createClient } from '@/lib/supabase/client'
|
||||
import { BankSelector, type Bank } from './BankSelector'
|
||||
@@ -18,8 +19,10 @@ export default function BankingSettingsPanel() {
|
||||
const { toast } = useToast()
|
||||
const supabase = createClient()
|
||||
|
||||
const { dialogProps, confirm } = useDestructiveConfirm()
|
||||
|
||||
const [bankConnections, setBankConnections] = useState<BankConnection[]>([])
|
||||
const [isSyncing, setIsSyncing] = useState(false)
|
||||
const [syncingConnectionId, setSyncingConnectionId] = useState<string | null>(null)
|
||||
const [isConnecting, setIsConnecting] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [selectedBank, setSelectedBank] = useState<Bank | null>(null)
|
||||
@@ -71,7 +74,7 @@ export default function BankingSettingsPanel() {
|
||||
}
|
||||
|
||||
async function handleSyncTransactions(connectionId: string) {
|
||||
setIsSyncing(true)
|
||||
setSyncingConnectionId(connectionId)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/extensions/ext/enable-banking/sync', {
|
||||
@@ -100,27 +103,41 @@ export default function BankingSettingsPanel() {
|
||||
})
|
||||
}
|
||||
|
||||
setIsSyncing(false)
|
||||
setSyncingConnectionId(null)
|
||||
}
|
||||
|
||||
async function handleDisconnectBank(connectionId: string) {
|
||||
const { error } = await supabase
|
||||
.from('bank_connections')
|
||||
.update({ status: 'revoked' })
|
||||
.eq('id', connectionId)
|
||||
const ok = await confirm({
|
||||
title: 'Koppla bort bank?',
|
||||
description: 'PSD2-samtycket kommer återkallas. Befintliga transaktioner påverkas inte.',
|
||||
confirmLabel: 'Koppla bort',
|
||||
variant: 'warning',
|
||||
})
|
||||
if (!ok) return
|
||||
|
||||
if (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: 'Kunde inte koppla bort bank',
|
||||
variant: 'destructive',
|
||||
try {
|
||||
const response = await fetch('/api/extensions/ext/enable-banking/disconnect', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ connection_id: connectionId }),
|
||||
})
|
||||
} else {
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
throw new Error(data.error || 'Disconnect failed')
|
||||
}
|
||||
|
||||
toast({
|
||||
title: 'Bank bortkopplad',
|
||||
description: 'Bankanslutningen har tagits bort',
|
||||
description: 'Bankanslutningen och PSD2-samtycket har återkallats',
|
||||
})
|
||||
fetchConnections()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: error instanceof Error ? error.message : 'Kunde inte koppla bort bank',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +153,8 @@ export default function BankingSettingsPanel() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<DestructiveConfirmDialog {...dialogProps} />
|
||||
|
||||
{/* Connected banks */}
|
||||
{activeConnections.length > 0 && (
|
||||
<Card>
|
||||
@@ -149,7 +168,7 @@ export default function BankingSettingsPanel() {
|
||||
connection={connection}
|
||||
onSync={handleSyncTransactions}
|
||||
onDisconnect={handleDisconnectBank}
|
||||
isSyncing={isSyncing}
|
||||
isSyncing={syncingConnectionId === connection.id}
|
||||
/>
|
||||
))}
|
||||
</CardContent>
|
||||
|
||||
Reference in New Issue
Block a user