MVP frontend+backend

This commit is contained in:
2026-06-14 09:58:30 +02:00
parent d9539f23cc
commit 772d853e37
56 changed files with 5099 additions and 207 deletions
+219
View File
@@ -0,0 +1,219 @@
/**
* Dev-only scraper test harness — /dev/fetch
*
* ⚠️ This page is mounted ONLY in DEV mode (import.meta.env.DEV).
* It is never included in production builds.
*
* Usage:
* 1. Run `npm run login` in the frontend directory.
* 2. Log in through the normal app flow first (the scraper endpoints require auth).
* 3. Click "Refresh" — the backend resolves your IDs automatically.
* 4. Scan the QR with BankID and watch status transitions until data appears.
*
* This runs the REAL FK scraper against the REAL Försäkringskassan site with YOUR BankID.
* Only run it intentionally. SCRAPER_ENABLED=true is set by npm run login:backend.
*/
import { useState, useEffect } from 'react'
import * as api from '@/services/api'
import { useScraperFetch } from '@/hooks'
import { isUnauthorized } from '@/services/errors'
export function DevFetch() {
const [context, setContext] = useState<api.UserContext | null>(null)
const [contextError, setContextError] = useState<string | null>(null)
const [childName, setChildName] = useState('')
const [childBirth, setChildBirth] = useState('')
const [creatingChild, setCreatingChild] = useState(false)
const scraper = useScraperFetch()
const loadContext = async () => {
try {
const ctx = await api.getUserContext()
setContext(ctx)
setContextError(null)
} catch (err) {
if (isUnauthorized(err)) {
setContextError('Not authenticated')
} else {
setContextError(err instanceof Error ? err.message : 'Failed to load context')
}
}
}
useEffect(() => {
loadContext()
}, [])
const handleRefresh = async () => {
await scraper.start()
}
const handleCreateChild = async () => {
if (!context || !childName || !childBirth) return
setCreatingChild(true)
try {
await api.createChild({
workspace_id: context.workspace_id,
name: childName,
birth_date: childBirth,
})
await loadContext()
setChildName('')
setChildBirth('')
} catch (err) {
setContextError(err instanceof Error ? err.message : 'Failed to create child')
} finally {
setCreatingChild(false)
}
}
const hasChildren = context && context.children.length > 0
return (
<div style={{ fontFamily: 'monospace', maxWidth: 700, margin: '2rem auto', padding: '1rem' }}>
<h1 style={{ fontSize: '1.2rem', marginBottom: 4 }}>🔧 FK Scraper Test Harness</h1>
<p style={{ color: '#888', fontSize: '0.85rem', marginBottom: '1.5rem' }}>
Dev-only. Runs the real FK scraper against Försäkringskassan with your BankID.
<br />
Log in through the normal app first — these endpoints require an app session.
</p>
{contextError && (
<div style={{ background: '#fee', border: '1px solid #f88', padding: '0.75rem', marginBottom: '1rem' }}>
<strong>Setup-fel:</strong> {contextError}
{scraper.isUnauthorized && (
<div style={{ marginTop: '0.5rem' }}>
<a href="/auth" style={{ color: '#0066cc', textDecoration: 'underline' }}>
Logga in först
</a>
</div>
)}
</div>
)}
{context && (
<div style={{ background: '#f6f8fa', border: '1px solid #ddd', padding: '0.75rem', marginBottom: '1rem', fontSize: '0.8rem' }}>
<div style={{ marginBottom: 4 }}>
<strong>Workspace:</strong> {context.workspace_id}
</div>
<div style={{ marginBottom: 4 }}>
<strong>PersonalData:</strong> {context.personal_data_uuid}
</div>
<div>
<strong>Barn:</strong>{' '}
{context.children.length === 0
? <span style={{ color: '#c00' }}>Inga barn registrerade</span>
: context.children.map(c => `${c.name} (${c.birth_date})`).join(', ')}
</div>
</div>
)}
{context && !hasChildren && (
<div style={{ background: '#fffbe6', border: '1px solid #ffe58f', padding: '0.75rem', marginBottom: '1rem' }}>
<div style={{ marginBottom: '0.5rem', fontWeight: 'bold', fontSize: '0.9rem' }}>Registrera ett barn för att kunna köra scrapern</div>
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', alignItems: 'center' }}>
<input
placeholder="Barnets namn"
value={childName}
onChange={e => setChildName(e.target.value)}
style={{ padding: '0.4rem', fontFamily: 'monospace', fontSize: '0.85rem' }}
/>
<input
type="date"
placeholder="Födelsedatum"
value={childBirth}
onChange={e => setChildBirth(e.target.value)}
style={{ padding: '0.4rem', fontFamily: 'monospace', fontSize: '0.85rem' }}
/>
<button
onClick={handleCreateChild}
disabled={creatingChild || !childName || !childBirth}
style={{ padding: '0.4rem 1rem', cursor: 'pointer' }}
>
{creatingChild ? 'Skapar…' : 'Skapa barn'}
</button>
</div>
</div>
)}
<div style={{ marginBottom: '1.5rem' }}>
<button
onClick={handleRefresh}
disabled={!hasChildren}
style={{ padding: '0.5rem 1.5rem', cursor: hasChildren ? 'pointer' : 'not-allowed', fontWeight: 'bold', opacity: hasChildren ? 1 : 0.5 }}
>
Refresh
</button>
</div>
{scraper.resolvedIds && (
<div style={{ color: '#aaa', fontSize: '0.75rem', marginBottom: '1rem', lineHeight: 1.6 }}>
<div>workspace_id: {scraper.resolvedIds.workspace_id}</div>
<div>personal_data_uuid: {scraper.resolvedIds.personal_data_uuid}</div>
<div>child_id: {scraper.resolvedIds.child_id}</div>
</div>
)}
{scraper.error && (
<div style={{ background: '#fee', border: '1px solid #f88', padding: '0.75rem', marginBottom: '1rem' }}>
<strong>Fel:</strong> {scraper.error}
</div>
)}
{scraper.pollData && (
<div>
<div style={{ marginBottom: '0.75rem' }}>
<strong>Status:</strong>{' '}
<span style={{ color: scraper.pollData.status === 'done' ? 'green' : scraper.pollData.status === 'error' ? 'red' : 'inherit' }}>
{scraper.pollData.status}
</span>
{scraper.pollData.message && (
<span style={{ color: '#888', marginLeft: '0.5rem' }}>— {scraper.pollData.message}</span>
)}
</div>
{scraper.pollData.status === 'awaiting_scan' && scraper.pollData.qr_data && (
<div style={{ marginBottom: '1rem' }}>
<p style={{ margin: '0 0 0.5rem', fontSize: '0.9rem' }}>
Öppna BankID-appen och skanna QR-koden (uppdateras automatiskt):
</p>
<img
src={`data:image/png;base64,${scraper.pollData.qr_data}`}
alt="BankID QR-kod"
style={{ width: 220, height: 220, imageRendering: 'pixelated', border: '1px solid #ccc' }}
/>
</div>
)}
{(scraper.pollData.status === 'authenticating' || scraper.pollData.status === 'fetching') && (
<p style={{ color: '#555' }}>⏳ {scraper.pollData.status === 'authenticating' ? 'Autentiserar…' : 'Hämtar data från FK…'}</p>
)}
{scraper.pollData.status === 'done' && scraper.pollData.result && (
<div>
<p style={{ color: 'green', marginBottom: '0.5rem' }}>✅ Klar! Hämtad data:</p>
<pre style={{ background: '#f6f8fa', padding: '1rem', overflow: 'auto', fontSize: '0.8rem', border: '1px solid #ddd' }}>
{JSON.stringify(scraper.pollData.result, null, 2)}
</pre>
</div>
)}
{scraper.pollData.status === 'cancelled' && (
<p style={{ color: '#888' }}>Sessionen avbröts.</p>
)}
{scraper.isRunning && (
<button
onClick={scraper.cancel}
style={{ marginTop: '0.5rem', padding: '0.4rem 0.8rem', cursor: 'pointer', background: '#fee', border: '1px solid #faa' }}
>
Avbryt
</button>
)}
</div>
)}
</div>
)
}