import { useState, useEffect } from 'react' import { BrowserRouter, Routes, Route, NavLink, Navigate } from 'react-router-dom' import { api, setCredentials, clearCredentials } from './api' import Login from './pages/Login' import Sources from './pages/Sources' import Import from './pages/Import' import Rules from './pages/Rules' import Mappings from './pages/Mappings' import Records from './pages/Records' import Log from './pages/Log' import Pivot from './pages/Pivot' import Remap from './pages/Remap' import Stacks from './pages/Stacks' const NAV = [ { to: '/sources', label: 'Sources' }, { to: '/import', label: 'Import' }, { to: '/rules', label: 'Rules' }, { to: '/mappings', label: 'Mappings' }, { to: '/remap', label: 'Remap' }, { to: '/records', label: 'Records' }, { to: '/pivot', label: 'Pivot' }, { to: '/stacks', label: 'Stacks' }, { to: '/log', label: 'Log' }, ] export default function App() { const [authed, setAuthed] = useState(false) const [loginUser, setLoginUser] = useState('') const [sources, setSources] = useState([]) const [source, setSource] = useState(() => localStorage.getItem('selectedSource') || '') const [sidebarOpen, setSidebarOpen] = useState(false) // Sets of names whose dfv view is out of sync with current definitions const [staleSources, setStaleSources] = useState(new Set()) const [staleStacks, setStaleStacks] = useState(new Set()) const [reprocessSources, setReprocessSources] = useState(new Set()) const [generating, setGenerating] = useState({}) // { 'source:name': true } async function handleLogin(user, pass) { setCredentials(user, pass) await api.getSources().then(s => { sessionStorage.setItem('df_user', user) sessionStorage.setItem('df_pass', pass) setSources(s) if (!source && s.length > 0) setSource(s[0].name) setAuthed(true) setLoginUser(user) }) } function handleLogout() { clearCredentials() sessionStorage.removeItem('df_user') sessionStorage.removeItem('df_pass') setAuthed(false) setLoginUser('') setSources([]) setStaleSources(new Set()) setStaleStacks(new Set()) setReprocessSources(new Set()) } // Load initial stale state from DB once on login useEffect(() => { if (!authed) return api.getStatus().then(s => { setStaleSources(new Set((s.stale_sources || []).map(x => x.name))) setStaleStacks(new Set((s.stale_stacks || []).map(x => x.name))) }).catch(() => {}) }, [authed]) function markSourceStale(name) { setStaleSources(prev => new Set([...prev, name])) } function markNeedsReprocess(name) { setReprocessSources(prev => new Set([...prev, name])) } async function handleReprocessSource(name) { setGenerating(g => ({ ...g, [`rp:${name}`]: true })) try { await api.reprocess(name) setReprocessSources(prev => { const n = new Set(prev); n.delete(name); return n }) } catch (e) { alert(e.message) } finally { setGenerating(g => { const n = { ...g }; delete n[`rp:${name}`]; return n }) } } function markStackStale(name) { setStaleStacks(prev => new Set([...prev, name])) } function clearStackStale(name) { setStaleStacks(prev => { const n = new Set(prev); n.delete(name); return n }) } async function handleGenerateSource(name) { setGenerating(g => ({ ...g, [`src:${name}`]: true })) try { await api.generateView(name) setStaleSources(prev => { const n = new Set(prev); n.delete(name); return n }) } catch (e) { alert(e.message) } finally { setGenerating(g => { const n = { ...g }; delete n[`src:${name}`]; return n }) } } async function handleGenerateStack(name) { setGenerating(g => ({ ...g, [`stk:${name}`]: true })) try { await api.generateStackView(name) setStaleStacks(prev => { const n = new Set(prev); n.delete(name); return n }) } catch (e) { alert(e.message) } finally { setGenerating(g => { const n = { ...g }; delete n[`stk:${name}`]; return n }) } } // On mount, restore session if credentials are saved useEffect(() => { const user = sessionStorage.getItem('df_user') const pass = sessionStorage.getItem('df_pass') if (user && pass) handleLogin(user, pass).catch(() => handleLogout()) }, []) useEffect(() => { if (source) localStorage.setItem('selectedSource', source) }, [source]) if (!authed) return const sidebar = (
{/* Header */}
Dataflow
{loginUser}
{/* Source selector */}
setSidebarOpen(false)}>+
{/* Nav */}
) return (
{/* Mobile overlay */} {sidebarOpen && (
setSidebarOpen(false)} /> )} {/* Sidebar — fixed on mobile, static on desktop */}
{sidebar}
{/* Main */}
{/* Mobile top bar */}
Dataflow
{(staleSources.size > 0 || staleStacks.size > 0) && (
View out of sync: {[...staleSources].map(name => ( {name} ))} {staleSources.size > 0 && staleStacks.size > 0 && |} {[...staleStacks].map(name => ( stack: {name} ))}
)} {reprocessSources.size > 0 && (
Mappings updated: {[...reprocessSources].map(name => ( {name} ))}
)}
} /> } /> } /> } /> } /> } /> } /> } /> } /> } />
) }