Bug/mcp connection issue (#541)

* feat(api): implement caching and logging in health check endpoint

- Added in-memory caching for health check responses to reduce load on Postgres.
- Introduced logging for error handling in health check.
- Updated response structure to exclude error details from public responses.

feat(api): enhance OAuth consent UI and scope handling

- Improved consent UI to reflect exact requested scopes and added better user guidance.
- Updated scope handling logic to ensure least-privilege access.
- Enhanced styling for better user experience and accessibility.

chore(docker): improve security and resource management in Docker setup

- Updated Docker Compose configuration to enforce read-only file systems and resource limits.
- Added health checks and logging options for better observability.
- Introduced optional Caddy reverse proxy for TLS termination.

fix(migrations): resolve ambiguity in create_company_with_owner function

- Dropped orphaned 3-arg overload of create_company_with_owner function.
- Recreated canonical 4-arg version with cash account seeding logic.
- Ensured proper permissions for function execution in Postgres.

* feat: enhance security checks for team membership in company creation

* test: add CSP tests for OAuth authorization endpoint

* feat: enhance error handling and reporting in bank file import process
This commit is contained in:
Mattsson
2026-05-20 10:08:32 +02:00
committed by GitHub
parent 9aced4790c
commit 566ed72984
14 changed files with 674 additions and 48 deletions
+72 -20
View File
@@ -22,9 +22,27 @@ import {
} from '@/components/ui/table'
import { ArrowLeft, ArrowRight, Columns3 } from 'lucide-react'
import { formatCurrency } from '@/lib/utils'
import { getCSVPreview } from '@/lib/import/bank-file/formats/generic-csv'
import { getCSVPreview, normalizeMinusSign } from '@/lib/import/bank-file/formats/generic-csv'
import type { GenericCSVColumnMapping } from '@/lib/import/bank-file/types'
const HEADER_KEYWORDS = [
'datum',
'bokföringsdag',
'bokforingsdag',
'transaktionsdatum',
'reskontradatum',
'beskrivning',
'belopp',
'transaktion',
'text',
'mottagare',
'saldo',
'valuta',
'amount',
'description',
'date',
]
interface BankFileColumnMappingStepProps {
rawFileContent: string
onConfirm: (mapping: GenericCSVColumnMapping) => void
@@ -59,39 +77,70 @@ export default function BankFileColumnMappingStep({
const [decimalSep, setDecimalSep] = useState<',' | '.'>(',')
const [dateFormat, setDateFormat] = useState<string>('YYYY-MM-DD')
// Re-parse headers and preview whenever delimiter or file content changes
// Re-parse headers and preview whenever delimiter or file content changes.
// Pull a generous slice (30 rows) so we can scan past metadata preambles like
// Northmill's 5-line Kontonummer/Saldo/Kontohavare/Org.Nr/Period header.
const parsedRows = useMemo(
() => getCSVPreview(rawFileContent, delimiter, 10),
() => getCSVPreview(rawFileContent, delimiter, 30),
[rawFileContent, delimiter]
)
// Auto-detect whether the first row is a header: if any cell on row 0 looks
// like a date (YYYY-MM-DD, DD.MM.YYYY, DD/MM/YYYY, YYYYMMDD), it's data, not a header.
// Users can still override via the switch.
// Auto-detect the header row index by scanning for a row whose cells
// contain known column-name keywords (bokföringsdag, beskrivning, belopp, …).
// A row needs ≥ 2 keyword hits to qualify, which excludes metadata rows where
// only the label cell happens to match (e.g. "Saldo,251495,41,SEK").
// Falls back to row 0 if nothing qualifies — preserves prior behavior for
// simple files where the header truly is the first row.
const DATE_PATTERNS = [/^\d{4}-\d{2}-\d{2}$/, /^\d{2}[./]\d{2}[./]\d{4}$/, /^\d{8}$/]
const detectedHeaderRow = useMemo(() => {
let best: { idx: number; score: number; cells: number } | null = null
for (let i = 0; i < Math.min(parsedRows.length, 20); i++) {
const row = parsedRows[i]
if (!row || row.length < 2) continue
const hits = row.filter((cell) => {
const c = cell.trim().toLowerCase()
return c.length > 0 && HEADER_KEYWORDS.some((kw) => c === kw || c.includes(kw))
}).length
if (hits < 2) continue
if (!best || hits > best.score || (hits === best.score && row.length > best.cells)) {
best = { idx: i, score: hits, cells: row.length }
}
}
return best?.idx ?? 0
}, [parsedRows])
// Detect whether the file actually has a header row at all: if the auto-detect
// landed on row 0 but row 0 already looks like data (any cell is a date), assume
// no header. Otherwise trust the detection.
const detectedHasHeader = useMemo(() => {
const firstRow = parsedRows[0]
if (!firstRow) return true
const hasDateCell = firstRow.some((cell) =>
const headerRow = parsedRows[detectedHeaderRow]
if (!headerRow) return true
const looksLikeData = headerRow.some((cell) =>
DATE_PATTERNS.some((re) => re.test(cell.trim()))
)
return !hasDateCell
}, [parsedRows])
return !looksLikeData
}, [parsedRows, detectedHeaderRow])
const [hasHeaderOverride, setHasHeaderOverride] = useState<boolean | null>(null)
const hasHeader = hasHeaderOverride ?? detectedHasHeader
// skip_rows = number of rows to skip before transaction data starts.
// When hasHeader: skip past the header row (detectedHeaderRow + 1).
// When no header: skip nothing — data starts at row 0.
const skipRows = hasHeader ? detectedHeaderRow + 1 : 0
const columnHeaders = useMemo(() => {
if (hasHeader && parsedRows[0]) return parsedRows[0]
if (hasHeader && parsedRows[detectedHeaderRow]) return parsedRows[detectedHeaderRow]
const count = parsedRows[0]?.length ?? 0
return Array.from({ length: count }, (_, i) => `Kolumn ${i + 1}`)
}, [parsedRows, hasHeader])
}, [parsedRows, hasHeader, detectedHeaderRow])
const dataRows = hasHeader ? parsedRows.slice(1) : parsedRows
const dataRows = hasHeader ? parsedRows.slice(detectedHeaderRow + 1) : parsedRows
// Auto-guess date/description/amount columns from the first data row.
// Only used as initial defaults — user can override any pick.
const AMOUNT_RE = /^-?\d+([.,]\d+)?$/
// Match ASCII and Unicode minus; some banks (e.g. Northmill) use U+2212.
const AMOUNT_RE = /^[-\u2212\u2013\u2014\u2010]?\d+([.,]\d+)?$/
useEffect(() => {
if (dateCol !== -1 || descCol !== -1 || amountCol !== -1) return
const sample = dataRows[0]
@@ -124,7 +173,7 @@ export default function BankFileColumnMappingStep({
...(balanceCol >= 0 && { balance: balanceCol }),
delimiter,
decimal_separator: decimalSep,
skip_rows: hasHeader ? 1 : 0,
skip_rows: skipRows,
date_format: dateFormat,
}
onConfirm(mapping)
@@ -150,7 +199,9 @@ export default function BankFileColumnMappingStep({
<div className="space-y-0.5">
<Label htmlFor="has-header">Har filen rubrikrad?</Label>
<p className="text-xs text-muted-foreground">
Slå av om filen saknar rubrikrad och första raden redan innehåller transaktionsdata.
{hasHeader && detectedHeaderRow > 0
? `Hoppar över ${detectedHeaderRow} metadatarader. Rubrikraden upptäcktes på rad ${detectedHeaderRow + 1}.`
: 'Slå av om filen saknar rubrikrad och första raden redan innehåller transaktionsdata.'}
</p>
</div>
<Switch id="has-header" checked={hasHeader} onCheckedChange={setHasHeaderOverride} />
@@ -350,11 +401,12 @@ export default function BankFileColumnMappingStep({
</TableRow>
</TableHeader>
<TableBody>
{dataRows.slice(0, 5).map((row, i) => {
{dataRows.slice(0, 5).map((row, i) => {
const amountStr = row[amountCol] || '0'
const normalizedAmountStr = normalizeMinusSign(amountStr)
const amount = decimalSep === ','
? parseFloat(amountStr.replace(/\s/g, '').replace(',', '.'))
: parseFloat(amountStr.replace(/\s/g, ''))
? parseFloat(normalizedAmountStr.replace(/\s/g, '').replace(',', '.'))
: parseFloat(normalizedAmountStr.replace(/\s/g, ''))
return (
<TableRow key={i}>