Feature/quick review scroll (#110)

* fix: allow scrolling in QuickReviewDialog when content overflows viewport

The dialog had no max-height or overflow handling, so tall content (e.g.
extern representation with warnings + journal preview) was clipped with
no way to scroll.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add detailed error logging to Enable Banking integration

Adds structured console logging with full context (status codes,
response bodies, connection IDs, account UIDs, stack traces) to all
Enable Banking operations: connect, callback, sync, disconnect,
and the API client retry/error paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add detailed error logging to login and register flows

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: implement KPI settings management with preferences and dynamic KPI display

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-03-23 20:04:34 +01:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 4d4a9a40c9
commit c1630068b3
15 changed files with 1097 additions and 130 deletions
@@ -53,6 +53,11 @@ export default function BankingSettingsPanel() {
setConnectingBankName(bank.name)
try {
console.log('[enable-banking] Initiating bank connection', {
bankName: bank.name,
bankCountry: bank.country,
})
const response = await fetch('/api/extensions/ext/enable-banking/connect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -62,11 +67,26 @@ export default function BankingSettingsPanel() {
const data = await response.json()
if (!response.ok) {
console.error('[enable-banking] Connect request failed', {
status: response.status,
statusText: response.statusText,
error: data.error,
bankName: bank.name,
})
throw new Error(data.error)
}
console.log('[enable-banking] Redirecting to bank authorization', {
connectionId: data.connection_id,
hasAuthUrl: !!data.authorization_url,
})
window.location.href = data.authorization_url
} catch (error) {
console.error('[enable-banking] Connect flow failed', {
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
bankName: bank.name,
})
toast({
title: 'Fel',
description: error instanceof Error ? error.message : 'Kunde inte ansluta bank',
@@ -82,6 +102,8 @@ export default function BankingSettingsPanel() {
setSyncingConnectionId(connectionId)
try {
console.log('[enable-banking] Starting sync', { connectionId })
const response = await fetch('/api/extensions/ext/enable-banking/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -91,9 +113,21 @@ export default function BankingSettingsPanel() {
const data = await response.json()
if (!response.ok) {
console.error('[enable-banking] Sync request failed', {
status: response.status,
statusText: response.statusText,
error: data.error,
connectionId,
})
throw new Error(data.error)
}
console.log('[enable-banking] Sync completed', {
connectionId,
imported: data.imported,
duplicates: data.duplicates,
})
toast({
title: 'Synkronisering klar',
description: `${data.imported} nya transaktioner importerade`,
@@ -102,6 +136,11 @@ export default function BankingSettingsPanel() {
setShowCsvFallback(false)
fetchConnections()
} catch (error) {
console.error('[enable-banking] Sync flow failed', {
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
connectionId,
})
toast({
title: 'Fel',
description: error instanceof Error ? error.message : 'Synkronisering misslyckades',
@@ -123,6 +162,8 @@ export default function BankingSettingsPanel() {
if (!ok) return
try {
console.log('[enable-banking] Disconnecting bank', { connectionId })
const response = await fetch('/api/extensions/ext/enable-banking/disconnect', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
@@ -131,15 +172,27 @@ export default function BankingSettingsPanel() {
if (!response.ok) {
const data = await response.json()
console.error('[enable-banking] Disconnect request failed', {
status: response.status,
statusText: response.statusText,
error: data.error,
connectionId,
})
throw new Error(data.error || 'Disconnect failed')
}
console.log('[enable-banking] Bank disconnected', { connectionId })
toast({
title: 'Bank bortkopplad',
description: 'Bankanslutningen och PSD2-samtycket har återkallats',
})
fetchConnections()
} catch (error) {
console.error('[enable-banking] Disconnect flow failed', {
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
connectionId,
})
toast({
title: 'Fel',
description: error instanceof Error ? error.message : 'Kunde inte koppla bort bank',
+31 -6
View File
@@ -148,8 +148,14 @@ export const enableBankingExtension: Extension = {
.single()
if (error) {
log.error('Database error:', error)
throw new Error('Failed to store connection')
log.error('[enable-banking] Database error storing connection', {
errorMessage: error.message,
errorCode: error.code,
errorDetails: error.details,
user_id: user.id,
bank: aspsp_name,
})
throw new Error(`Failed to store connection: ${error.message}`)
}
return NextResponse.json({
@@ -157,7 +163,14 @@ export const enableBankingExtension: Extension = {
authorization_url: url,
})
} catch (error) {
log.error('Bank connection error:', error)
log.error('[enable-banking] Connect handler error', {
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
name: error instanceof Error ? error.name : undefined,
user_id: user.id,
aspsp_name,
aspsp_country,
})
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Connection failed' },
{ status: 500 }
@@ -256,7 +269,15 @@ export const enableBankingExtension: Extension = {
last_synced_at: syncedAt,
})
} catch (error) {
log.error('Sync error:', error)
log.error('[enable-banking] Sync handler error', {
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
name: error instanceof Error ? error.name : undefined,
user_id: user.id,
connection_id,
connectionStatus: connection.status,
bankName: connection.bank_name,
})
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Sync failed' },
{ status: 500 }
@@ -298,8 +319,12 @@ export const enableBankingExtension: Extension = {
try {
await deleteSession(connection.session_id)
} catch (error) {
// Consent may already be expired — log and continue
log.error('Failed to revoke PSD2 session (may be expired):', error)
log.error('[enable-banking] Failed to revoke PSD2 session (may be expired)', {
message: error instanceof Error ? error.message : String(error),
sessionId: connection.session_id,
connectionId: connection_id,
connectionStatus: connection.status,
})
}
}
@@ -187,6 +187,10 @@ async function authenticatedFetchWithRetry(
try {
const response = await authenticatedFetch(endpoint, options)
if (attempt < MAX_RETRIES && [429, 502, 503, 504].includes(response.status)) {
console.warn(`[enable-banking] Retrying ${endpoint} (attempt ${attempt + 1}/${MAX_RETRIES})`, {
status: response.status,
statusText: response.statusText,
})
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS * (attempt + 1)))
continue
}
@@ -194,9 +198,16 @@ async function authenticatedFetchWithRetry(
} catch (error: unknown) {
const isAbort = error instanceof Error && error.name === 'AbortError'
if (attempt < MAX_RETRIES && isAbort) {
console.warn(`[enable-banking] Request timeout, retrying ${endpoint} (attempt ${attempt + 1}/${MAX_RETRIES})`)
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS * (attempt + 1)))
continue
}
console.error(`[enable-banking] Request failed for ${endpoint}`, {
attempt,
error: error instanceof Error ? error.message : String(error),
name: error instanceof Error ? error.name : undefined,
isTimeout: isAbort,
})
throw error
}
}
@@ -220,9 +231,17 @@ export async function getASPSPs(country: string = 'SE'): Promise<ASPSP[]> {
const response = await authenticatedFetchWithRetry(`/aspsps?${params.toString()}`)
if (!response.ok) {
const error = await response.text()
console.error('Failed to fetch ASPSPs:', error)
throw new Error('Failed to fetch banks')
const body = await response.text()
console.error('[enable-banking] getASPSPs failed', {
status: response.status,
statusText: response.statusText,
body,
country,
psuType,
sandbox: isSandbox,
apiUrl: ENABLE_BANKING_API_URL,
})
throw new Error(`Failed to fetch banks (${response.status})`)
}
const data = await response.json()
@@ -294,9 +313,19 @@ export async function startAuthorization(
})
if (!response.ok) {
const error = await response.text()
console.error('Failed to start authorization:', error)
throw new Error('Failed to start bank connection')
const body = await response.text()
console.error('[enable-banking] startAuthorization failed', {
status: response.status,
statusText: response.statusText,
body,
aspspName,
aspspCountry,
psuType,
redirectUrl,
apiUrl: ENABLE_BANKING_API_URL,
requestBody: JSON.stringify(requestBody),
})
throw new Error(`Failed to start bank connection (${response.status}): ${body}`)
}
return response.json()
@@ -314,9 +343,16 @@ export async function createSession(code: string): Promise<SessionResponse> {
})
if (!response.ok) {
const error = await response.text()
console.error('Failed to create session:', error)
throw new Error('Failed to create bank session')
const body = await response.text()
console.error('[enable-banking] createSession failed', {
status: response.status,
statusText: response.statusText,
body,
hasCode: !!code,
codeLength: code?.length,
apiUrl: ENABLE_BANKING_API_URL,
})
throw new Error(`Failed to create bank session (${response.status}): ${body}`)
}
return response.json()
@@ -331,7 +367,14 @@ export async function getSession(sessionId: string): Promise<SessionResponse> {
const response = await authenticatedFetchWithRetry(`/sessions/${sessionId}`)
if (!response.ok) {
throw new Error('Failed to get session')
const body = await response.text()
console.error('[enable-banking] getSession failed', {
status: response.status,
statusText: response.statusText,
body,
sessionId,
})
throw new Error(`Failed to get session (${response.status}): ${body}`)
}
return response.json()
@@ -348,7 +391,14 @@ export async function deleteSession(sessionId: string): Promise<void> {
})
if (!response.ok) {
throw new Error('Failed to revoke session')
const body = await response.text()
console.error('[enable-banking] deleteSession failed', {
status: response.status,
statusText: response.statusText,
body,
sessionId,
})
throw new Error(`Failed to revoke session (${response.status}): ${body}`)
}
}
@@ -361,7 +411,14 @@ export async function getAccountBalances(accountUid: string): Promise<Balance[]>
const response = await authenticatedFetchWithRetry(`/accounts/${accountUid}/balances`)
if (!response.ok) {
throw new Error('Failed to get account balances')
const body = await response.text()
console.error('[enable-banking] getAccountBalances failed', {
status: response.status,
statusText: response.statusText,
body,
accountUid,
})
throw new Error(`Failed to get account balances (${response.status}): ${body}`)
}
const data: BalanceResponse = await response.json()
@@ -417,7 +474,17 @@ export async function getAccountTransactions(
const response = await authenticatedFetchWithRetry(endpoint)
if (!response.ok) {
throw new Error('Failed to get transactions')
const body = await response.text()
console.error('[enable-banking] getAccountTransactions failed', {
status: response.status,
statusText: response.statusText,
body,
accountUid,
dateFrom,
dateTo,
hasContinuationKey: !!continuationKey,
})
throw new Error(`Failed to get transactions (${response.status}): ${body}`)
}
return response.json()
@@ -482,7 +549,18 @@ export async function getAllTransactionsWithRaw(
const response = await authenticatedFetchWithRetry(endpoint)
if (!response.ok) {
throw new Error('Failed to get transactions')
const body = await response.text()
console.error('[enable-banking] getAllTransactionsWithRaw failed', {
status: response.status,
statusText: response.statusText,
body,
accountUid,
dateFrom,
dateTo,
page,
hasContinuationKey: !!continuationKey,
})
throw new Error(`Failed to get transactions (${response.status}): ${body}`)
}
const rawText = await response.text()
@@ -38,12 +38,27 @@ export async function syncAccountTransactions(
toDate: string,
ingest: IngestFn = defaultIngest
): Promise<SyncResult> {
console.log('[enable-banking] syncAccountTransactions starting', {
connectionId,
accountUid: account.uid,
accountIban: account.iban,
fromDate,
toDate,
})
const { transactions, rawPages } = await getAllTransactionsWithRaw(
account.uid,
fromDate,
toDate,
)
console.log('[enable-banking] Fetched transactions from API', {
connectionId,
accountUid: account.uid,
transactionCount: transactions.length,
rawPageCount: rawPages.length,
})
const bankTransactions = transactions.map(tx => convertTransaction(tx, account.currency))
// Convert Enable Banking format to generic RawTransaction
@@ -62,6 +77,14 @@ export async function syncAccountTransactions(
const ingestResult = await ingest(supabase, userId, rawTransactions)
console.log('[enable-banking] Ingest result', {
connectionId,
accountUid: account.uid,
imported: ingestResult.imported,
duplicates: ingestResult.duplicates,
errors: ingestResult.errors,
})
// Archive raw PSD2 API responses as räkenskapsinformation (BFL 7 kap)
for (let i = 0; i < rawPages.length; i++) {
try {