DEV Community

iron_man
iron_man

Posted on Fully Autonomous

DEV-Part-2-Frontend-and-Sample-Files.md

Oracle Schema Hardening — Part 2: Frontend and Sample Files

Each heading below is a file path relative to the project root. Create that file and copy only the code inside its code block. Both parts belong to the same Spring Boot and React project.

File list

  • fixtures/library/TR/db-links/definitions.json
  • fixtures/library/TR/grants/AUD_TXN.sql
  • fixtures/library/TR/manifest.json
  • fixtures/library/TR/synonyms/AUD_TXN.sql
  • fixtures/library/TR/tables/AUD_TXN.sql
  • fixtures/tnsnames.ora
  • frontend/package.json
  • frontend/src/App.tsx
  • frontend/src/api.ts
  • frontend/src/main.tsx
  • frontend/src/styles.css
  • frontend/src/types.ts
  • frontend/tsconfig.json
  • frontend/vite.config.ts

fixtures/library/TR/db-links/definitions.json

{
  "linkName": "CSS_LINK",
  "remoteSegment": "CSS",
  "tnsRule": "SELECTED",
  "remoteAlias": null,
  "verification": "DUAL",
  "dependentObjects": ["PKG_SETTLEMENT"]
}
Enter fullscreen mode Exit fullscreen mode

fixtures/library/TR/grants/AUD_TXN.sql

GRANT SELECT ON ${TARGET_SCHEMA}.AUD_TXN TO ${BASE_SCHEMA}TRCUS;
Enter fullscreen mode Exit fullscreen mode

fixtures/library/TR/manifest.json

[
  {"id":"css-link","objectName":"CSS_LINK","objectType":"DATABASE LINK","file":"db-links/definitions.json","rank":1,"dependencies":[],"executionSegment":"CURRENT","sourcePreference":"LIBRARY_FIRST","verification":"DB_LINK_HEALTHY"},
  {"id":"audit-table","objectName":"AUD_TXN","objectType":"TABLE","file":"tables/AUD_TXN.sql","rank":2,"dependencies":[],"executionSegment":"CURRENT","sourcePreference":"LIBRARY_FIRST","verification":"TABLE_EXISTS"},
  {"id":"audit-grant","objectName":"AUD_TXN","objectType":"GRANT","file":"grants/AUD_TXN.sql","rank":3,"dependencies":["audit-table"],"executionSegment":"TR","sourcePreference":"LIBRARY_FIRST","verification":"GRANT_EXISTS","relatedSegment":"TRCUS","privilege":"SELECT"},
  {"id":"audit-synonym","objectName":"AUD_TXN","objectType":"SYNONYM","file":"synonyms/AUD_TXN.sql","rank":3,"dependencies":["audit-grant"],"executionSegment":"TRCUS","sourcePreference":"LIBRARY_FIRST","verification":"SYNONYM_EXISTS","relatedSegment":"TR"}
]
Enter fullscreen mode Exit fullscreen mode

fixtures/library/TR/synonyms/AUD_TXN.sql

CREATE SYNONYM ${TARGET_SCHEMA}.AUD_TXN FOR ${BASE_SCHEMA}TR.AUD_TXN;
Enter fullscreen mode Exit fullscreen mode

fixtures/library/TR/tables/AUD_TXN.sql

-- Example fixture only. Replace with your reviewed audit-table definition.
CREATE TABLE ${TARGET_SCHEMA}.AUD_TXN (
  TXN_ID NUMBER NOT NULL,
  ACTION_NAME VARCHAR2(30),
  CHANGED_AT TIMESTAMP DEFAULT SYSTIMESTAMP
);
Enter fullscreen mode Exit fullscreen mode

fixtures/tnsnames.ora

# Demonstration only. No corporate endpoints.
DEMO141 =
  (DESCRIPTION =
    (ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521))
    (CONNECT_DATA = (SERVICE_NAME = DEMO)))
DEMO141_ALT =
  (DESCRIPTION =
    (ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1522))
    (CONNECT_DATA = (SERVICE_NAME = DEMO_ALT)))
Enter fullscreen mode Exit fullscreen mode

frontend/package.json

{
  "name": "oracle-hardening-dashboard",
  "private": true,
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "dev": "vite --host 127.0.0.1",
    "build": "tsc -b && vite build"
  },
  "dependencies": {
    "react": "19.1.1",
    "react-dom": "19.1.1",
    "lucide-react": "0.468.0"
  },
  "devDependencies": {
    "@types/react": "19.1.10",
    "@types/react-dom": "19.1.9",
    "@vitejs/plugin-react": "4.7.0",
    "typescript": "5.9.2",
    "vite": "6.4.1"
  },
  "engines": {
    "node": ">=22.12.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

frontend/src/App.tsx

import { useEffect, useState } from 'react';
import { Activity, ArrowDown, ArrowRight, Check, CheckCircle2, ChevronRight, Clock3, Code2, Database, Download, FileCode2, History, Layers3, LayoutDashboard, Link2, LoaderCircle, LockKeyhole, Play, RefreshCw, Search, Settings2, ShieldCheck, Terminal, Unplug, X } from 'lucide-react';
import { api, login, refreshCsrf } from './api';
import { nextBatch, type Action, type Config, type Run } from './types';
export default function App() {
    const [config, setConfig] = useState<Config | null>(null), [input, setInput] = useState('DEMO@141'), [alias, setAlias] = useState(''), [aliases, setAliases] = useState<string[]>([]), [segment, setSegment] = useState('TR');
    const [run, setRun] = useState<Run | null>(null), [busy, setBusy] = useState(''), [error, setError] = useState(''), [notice, setNotice] = useState(''), [selected, setSelected] = useState<Action | null>(null), [tab, setTab] = useState('workspace');
    const [history, setHistory] = useState<Run[]>([]), [approval, setApproval] = useState<Action[] | null>(null), [approved, setApproved] = useState(false), [adminOpen, setAdminOpen] = useState(false), [password, setPassword] = useState(''), [username, setUsername] = useState('admin');
    const [referenceInput, setReferenceInput] = useState('REFERENCE@141'), [referenceAlias, setReferenceAlias] = useState(''), [referenceAliases, setReferenceAliases] = useState<string[]>([]), [release, setRelease] = useState(''), [referencePreview, setReferencePreview] = useState<{
        sourceSchema: string;
        release: string;
        sql: string;
        checksum: string;
        message: string;
    } | null>(null), [filter, setFilter] = useState('');
    const [historical, setHistorical] = useState(false);
    const [compatible, setCompatible] = useState(false), [targetRelease, setTargetRelease] = useState('');
    const [candidates, setCandidates] = useState<{
        id: string;
        objectName: string;
        objectType: string;
        segment: string;
        sql: string;
        checksum: string;
    }[]>([]), [publishCandidate, setPublishCandidate] = useState<string | null>(null);
    async function task(name: string, fn: () => Promise<void>) { setBusy(name); setError(''); setNotice(''); try {
        await fn();
    }
    catch (e) {
        setError(e instanceof Error ? e.message : 'Operation failed');
    }
    finally {
        setBusy('');
    } }
    useEffect(() => { api<Config>('/config').then(c => { setConfig(c); setSegment(c.segments[0] || ''); }).catch(e => setError(e.message)); }, []);
    const batch = run ? nextBatch(run.actions) : [];
    const base = input.split('@')[0].trim().toUpperCase();
    const successes = run?.actions.filter(a => a.status === 'SUCCESS').length || 0;
    const prerequisites = run?.actions.filter(a => a.source === 'LIBRARY') || [];
    function clearRun() { setRun(null); setSelected(null); setApproval(null); setReferencePreview(null); setHistorical(false); }
    async function findTns() { await task('Finding TNS', async () => { const result = await api<{
        aliases: string[];
    }>('/tns?input=' + encodeURIComponent(input)); setAliases(result.aliases); setAlias(result.aliases.length === 1 ? result.aliases[0] : ''); if (!result.aliases.length)
        setNotice('No matching aliases. Ask your administrator to update the approved TNS file.'); }); }
    async function scan() { await task('Connecting and scanning', async () => { const target = { input, alias, segment }; await api('/connection/test', 'POST', target); const result = await api<Run>('/runs', 'POST', target); setRun(result); setSelected(result.actions[0] || null); setHistorical(false); setReferencePreview(null); }); }
    function review(actions: Action[]) { setApproval(actions); setApproved(false); }
    function exportRun() { if (!run)
        return; const link = document.createElement('a'); link.href = URL.createObjectURL(new Blob([JSON.stringify(run, null, 2)], { type: 'application/json' })); link.download = `hardening-${run.id}.json`; link.click(); URL.revokeObjectURL(link.href); }
    const visible = run?.actions.filter(a => (a.objectName + ' ' + a.objectType + ' ' + a.status).toLowerCase().includes(filter.toLowerCase())) || [];
    return <div className="app">
  <aside className="sidebar">
   <a className="brand" href="/" aria-label="SchemaWorks home"><span className="brand-mark"><Layers3 size={22}/></span><span>Schema<span className="brand-light">Works</span><small>ORACLE OPERATIONS</small></span></a>
   <div className="nav-label">WORKSPACE</div>
   <button className={'nav-item ' + (tab === 'workspace' ? 'active' : '')} onClick={() => setTab('workspace')}><LayoutDashboard size={18}/>Schema hardening<span className="nav-dot"/></button>
   <button className={'nav-item ' + (tab === 'history' ? 'active' : '')} onClick={() => task('Loading history', async () => { setHistory(await api<Run[]>('/history')); setTab('history'); })}><History size={18}/>Run history</button>
   <button className={'nav-item ' + (tab === 'library' ? 'active' : '')} onClick={() => task('Loading library', async () => { setTab('library'); if (config?.admin)
        setCandidates(await api('/admin/candidates')); })}><FileCode2 size={18}/>Fix library</button>
   <div className="nav-label tools-label">MANAGEMENT</div>
   <button className="nav-item" onClick={() => setAdminOpen(true)}><Settings2 size={18}/>Reference setup<LockKeyhole size={13}/></button>
   <div className="sidebar-bottom"><div className="connection-orbit"><Database size={21}/><span /></div><strong>Built for your environment</strong><p>One workspace. Every schema.<br />Changes stay in your control.</p><div className="sidebar-version"><span className="tiny-dot"/> LOCAL WORKSPACE <span>v0.1</span></div></div>
  </aside>
  <div className="main-shell">
   <header className="topbar"><div className="breadcrumbs">Operations <ChevronRight size={14}/><span>{tab === 'history' ? 'Run history' : tab === 'library' ? 'Fix library' : 'Schema hardening'}</span></div><div className="topbar-right"><button className="reference-status" onClick={() => setAdminOpen(true)}><span className={'tiny-dot ' + (config?.referenceReady ? 'green' : 'amber')}/>Reference: {config?.referenceReady ? 'Configured' : 'Not configured'}<ChevronRight size={13}/></button><span className="top-divider"/><button className="avatar" onClick={() => setAdminOpen(true)} aria-label="Account and reference settings">{config?.admin ? 'AD' : 'OP'}</button></div></header>
   <main>
    <div className="page-heading"><div><div className="eyebrow">DATABASE READINESS</div><h1>{tab === 'history' ? 'Run history' : tab === 'library' ? 'Reusable fix library' : 'Schema hardening'}</h1><p>{tab === 'history' ? 'A record of every reviewed change and verified result.' : tab === 'library' ? 'Reviewed fixes, organised by segment and dependency.' : 'From invalid objects to a ready schema. One reviewed step at a time.'}</p></div><div className="mode-badge"><span className="tiny-dot"/>{config?.mode === 'demo' ? 'DEMO ENVIRONMENT' : 'ORACLE ENVIRONMENT'}</div></div>
    {config?.mode === 'demo' && <div className="demo-banner"><ShieldCheck size={17}/><span><strong>Explore safely.</strong> This workspace uses simulated Oracle objects. No database is connected.</span><span className="banner-tag">SAMPLE DATA</span></div>}
    {error && <div className="message error" role="alert"><span>{error}</span><button onClick={() => setError('')} aria-label="Dismiss error"><X size={16}/></button></div>}
    {notice && <div className="message" role="status">{notice}</div>}
    {busy && <div className="busy-line" role="status"><LoaderCircle size={14} className="spin"/>{busy}</div>}
    {tab === 'workspace' && <>
     <section className="connection-card card"><div className="section-title"><span className="section-icon"><Database size={19}/></span><div><h2>Target connection</h2><p>Choose the schema you want to prepare.</p></div><span className="step-tag">01 / CONNECT</span></div>
      <div className="connection-fields"><label>Schema & server hint<div className="input-with-button"><input value={input} onChange={e => { setInput(e.target.value); setAliases([]); setAlias(''); clearRun(); }} onKeyDown={e => e.key === 'Enter' && findTns()} placeholder="BASE_SCHEMA@SERVER_HINT"/><button disabled={!!busy || !input} onClick={findTns} aria-label="Find matching TNS aliases"><Search size={16}/></button></div></label>
       <label>TNS alias<select value={alias} onChange={e => { setAlias(e.target.value); clearRun(); }}><option value="">{aliases.length ? 'Select matching alias' : 'Find TNS to get aliases'}</option>{aliases.map(a => <option key={a}>{a}</option>)}</select></label>
       <div className="segment-field"><span className="field-label">Segment</span><div className="segment-buttons" role="group" aria-label="Segment">{config?.segments.map(s => <button key={s} aria-pressed={segment === s} aria-label={s + ' segment'} className={segment === s ? 'chosen' : ''} onClick={() => { setSegment(s); clearRun(); }}>{s}</button>)}</div></div>
       <button className="primary connect-button" disabled={!!busy || !alias || !segment} onClick={scan}>{busy === 'Connecting and scanning' ? <LoaderCircle size={16} className="spin"/> : <Unplug size={16}/>}Connect & scan<ArrowRight size={16}/></button>
      </div><div className="connection-footer"><span>Target preview <code>{base && segment ? base + segment : ''}</code></span><span><LockKeyhole size={12}/>Connection and scanning are read-only</span></div>
     </section>
     <div className="metrics"><Metric label="Invalid objects" value={run ? String(run.remainingInvalid) : ''} icon={<Layers3 size={18}/>} note={run ? `${run.initialInvalid} at initial scan` : 'Waiting for first scan'} tone="orange"/><Metric label="Library prerequisites" value={run ? String(prerequisites.length) : ''} icon={<FileCode2 size={18}/>} note={run ? `${prerequisites.filter(a => a.status === 'SUCCESS').length} verified · repair before compilation` : 'Tables, grants & synonyms'}/><Metric label="Verified actions" value={run ? `${successes} / ${run.actions.length}` : ''} icon={<CheckCircle2 size={18}/>} note={run ? 'Results checked after execution' : 'Every change gets a verification'} tone="green"/><Metric label="Reference environment" value={config?.referenceReady ? 'Configured' : 'Not set'} icon={<Link2 size={18}/>} note="Same-segment fallback" small/></div>
     <div className="workspace-grid"><section className="plan-card card"><div className="plan-header"><div><h2>Repair plan <span className="count-pill">{run?.actions.length || 0}</span></h2><p>Prerequisites first. Dependencies always respected.</p></div><span className="step-tag">02 / REVIEW</span></div>
      <div className="plan-toolbar"><div className="search-field"><Search size={15}/><input aria-label="Search repair objects" placeholder="Search objects…" value={filter} onChange={e => setFilter(e.target.value)}/></div><span className="quiet-text">{historical ? 'Historical · read-only' : run ? 'Dependency order' : 'Awaiting connection'}</span></div>
      <div className="table-scroll"><table><thead><tr><th>ORDER</th><th>OBJECT / ACTION</th><th>SOURCE</th><th>STATUS</th><th /></tr></thead><tbody>{visible.map(a => <tr key={a.id} className={selected?.id === a.id ? 'selected' : ''} onClick={() => { setSelected(a); setReferencePreview(null); }} tabIndex={0} onKeyDown={e => { if (e.key === 'Enter') {
            setSelected(a);
            setReferencePreview(null);
        } }}><td><span className={'rank ' + (a.status === 'SUCCESS' ? 'rank-done' : '')}>{a.status === 'SUCCESS' ? <Check size={13}/> : String(a.rank).padStart(2, '0')}</span></td><td><strong>{a.objectName}</strong><small>{a.objectType.replaceAll('_', ' ')} <span>·</span> {a.executionSchema}</small></td><td><span className="source-tag">{a.source === 'LIBRARY' ? <FileCode2 size={12}/> : <Code2 size={12}/>} {a.source === 'LIBRARY' ? 'Library' : a.source === 'REFERENCE' ? 'Reference' : 'Compile'}</span></td><td><Status status={a.status}/></td><td><ChevronRight size={14}/></td></tr>)}</tbody></table></div>
      {!run && <div className="empty-plan"><div className="empty-icon"><Layers3 size={27}/></div><h3>Your repair plan starts here</h3><p>Connect a target schema to discover invalid objects<br />and build a plan from the shared library.</p><div className="empty-flow"><span>Connect</span><ChevronRight size={12}/><span>Scan</span><ChevronRight size={12}/><span>Review</span><ChevronRight size={12}/><span>Repair</span></div></div>}
      {run && !visible.length && <div className="empty-plan"><CheckCircle2 size={26}/><h3>{run.actions.length ? 'No matching objects' : 'No repairs required'}</h3></div>}
      <div className="plan-bottom"><div><ShieldCheck size={16}/><span>{run?.status === 'COMPLETED' ? 'All planned actions verified' : batch.length ? `${batch.length} action${batch.length > 1 ? 's' : ''} ready in rank ${batch[0].rank}` : 'Changes require your explicit approval'}</span></div><button className="primary" disabled={!!busy || !batch.length || historical || !config?.writesEnabled} onClick={() => review(batch)}><Play size={14}/>Repair all · review batch</button></div>
     </section>
     <aside className="detail-card card"><div className="detail-heading"><h2>Object details</h2><Code2 size={17}/></div>{selected ? <><div className="object-title"><span className="object-type">{selected.objectType}</span><h3>{selected.objectName}</h3><Status status={selected.status}/></div><dl><div><dt>Execution schema</dt><dd>{selected.executionSchema}</dd></div><div><dt>Repair source</dt><dd>{selected.source}</dd></div><div><dt>Verification</dt><dd>{selected.verification.replaceAll('_', ' ').toLowerCase()}</dd></div></dl><h4>WHY THIS ACTION</h4><p className="reason">{selected.reason}</p><h4>SCRIPT PREVIEW <span>READ-ONLY</span></h4><pre>{selected.sql || 'Manual review required.'}</pre><div className="dependency-note"><Link2 size={14}/>{selected.dependencies.length ? `${selected.dependencies.length} prerequisite${selected.dependencies.length > 1 ? 's' : ''} must complete first` : 'No prerequisite actions'}</div><div className="detail-actions"><button className="secondary" disabled={!!busy || historical || !batch.some(a => a.id === selected.id) || !config?.writesEnabled} onClick={() => review([selected])}>{selected.source === 'COMPILE' ? 'Compile selected' : 'Review library fix'}<ArrowRight size={14}/></button><button className="text-button" disabled={!!busy || historical || !config?.referenceReady} onClick={() => task('Reading reference', async () => setReferencePreview(await api(`/runs/${run!.id}/reference/${selected.id}`)))}>Compare reference</button><button className="text-button muted" disabled={!!busy || historical || !['PENDING', 'MANUAL'].includes(selected.status)} onClick={() => task('Skipping action', async () => { const next = await api<Run>(`/runs/${run!.id}/skip/${selected.id}`, 'POST'); setRun(next); setSelected(next.actions.find(a => a.id === selected.id) || null); })}>Skip action</button></div></> : <div className="empty-detail"><Search size={24}/><p>Select an object to inspect its errors, script, and dependencies.</p></div>}</aside></div>
     <section className="activity-card card"><div className="activity-heading"><div><Terminal size={18}/><h2>Run activity</h2><span className="quiet-text">{run ? run.id.slice(0, 8) : 'No active run'}</span></div><button className="text-button" disabled={!run} onClick={exportRun}><Download size={14}/>Export report</button></div>{!run ? <div className="activity-empty"><span className="tiny-dot"/>Ready when you are. Connect a schema to begin.</div> : <div className="events">{run.events.slice().reverse().map((e, i) => <div className="event" key={i}><time>{new Date(e.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}</time><span className={'event-dot ' + (e.status === 'FAILED' ? 'failed' : '')}/><strong>{e.action}</strong><span>{e.message}</span>{e.durationMs > 0 && <small>{e.durationMs} ms</small>}</div>)}</div>}</section>
     <footer><span><ShieldCheck size={13}/>Preview. Approve. Execute. Verify.</span><span>Oracle Schema Hardening · {config?.mode === 'demo' ? 'Demonstration workspace' : 'Configured Oracle environment'}</span></footer>
    </>}
    {tab === 'history' && <section className="card history-card"><div className="section-title"><History size={20}/><h2>Previous runs</h2><span className="quiet-text">Visible to your current account or demo session</span></div>{!history.length ? <div className="empty-plan"><Clock3 size={28}/><h3>No runs yet</h3><p>Completed scans and repair actions will appear here.</p></div> : <table><thead><tr><th>TARGET</th><th>STARTED</th><th>INVALID OBJECTS</th><th>STATUS</th><th /></tr></thead><tbody>{history.map(h => <tr key={h.id}><td><strong>{h.targetSchema}</strong><small>{h.alias} · {h.mode}</small></td><td>{new Date(h.createdAt).toLocaleString()}</td><td>{h.initialInvalid}{h.remainingInvalid}</td><td><Status status={h.status}/></td><td><button className="text-button" onClick={() => { setRun(h); setSelected(h.actions[0] || null); setHistorical(true); setTab('workspace'); }}>Open report<ArrowRight size={14}/></button></td></tr>)}</tbody></table>}</section>}
    {tab === 'library' && <section className="card library-info"><FileCode2 size={32}/><h2>A shared library, built on verified fixes</h2><p>Library manifests define each script’s execution segment, dependencies, and verification strategy. The backend reads the externally configured folder when you scan.</p><div className="library-steps"><div><span>01</span><h3>Individual scripts</h3><p>One logical object per SQL file. Tables, grants, and synonyms stay separate.</p></div><div><span>02</span><h3>Dependency order</h3><p>Only missing prerequisites from the selected segment policy become repair actions.</p></div><div><span>03</span><h3>Reviewed publication</h3><p>Reference fixes require compatibility review before they become reusable library entries.</p></div></div><div className="message">The included TR library contains example DB-link definitions, audit-table, grant, and synonym scripts. Replace these fixtures with your reviewed corporate scripts before Oracle use.</div>{config?.admin && <div className="candidate-list"><h3>Verified reference candidates</h3>{!candidates.length ? <p>No verified reference repairs are awaiting publication.</p> : candidates.map(c => <div className="approval-script" key={c.id}><div><strong>{c.segment} / {c.objectName}</strong><span>{c.objectType}</span></div><pre>{c.sql}</pre><label className="approval-check"><input type="checkbox" checked={publishCandidate === c.id} onChange={e => setPublishCandidate(e.target.checked ? c.id : null)}/>I reviewed this script for reuse in this segment.</label><button className="secondary" disabled={!!busy || publishCandidate !== c.id} onClick={() => task('Publishing versioned fix', async () => { await api(`/admin/candidates/${c.id}/publish`, 'POST', { checksum: c.checksum }); setCandidates(await api('/admin/candidates')); setPublishCandidate(null); setNotice('Versioned fix published to the segment manifest.'); })}>Publish to library</button></div>)}</div>}<button className="secondary" onClick={() => setTab('workspace')}>Back to workspace<ArrowRight size={15}/></button></section>}
   </main>
  </div>
  {approval && <div className="modal-overlay"><section className="modal approval-modal" role="dialog" aria-modal="true" aria-labelledby="approval-title"><button className="modal-close" onClick={() => setApproval(null)} disabled={!!busy} aria-label="Close approval"><X /></button><span className="modal-icon"><ShieldCheck size={26}/></span><div className="eyebrow">EXPLICIT APPROVAL</div><h2 id="approval-title">Review rank {approval[0].rank}</h2><p>{config?.mode === 'demo' ? 'These actions will update the simulated schema.' : 'These actions change the selected Oracle schemas. Oracle DDL may commit immediately.'}</p>{approval.map(a => <div className="approval-script" key={a.id}><div><strong>{a.objectName}</strong><code>{a.executionSchema}</code></div><pre>{a.sql}</pre><small>Verify: {a.verification.replaceAll('_', ' ')}</small></div>)}<label className="approval-check"><input type="checkbox" checked={approved} onChange={e => setApproved(e.target.checked)}/>I approve these exact scripts and their displayed target schemas.</label>{error && <div className="message error" role="alert">{error}</div>}<div className="modal-footer"><button className="secondary" onClick={() => setApproval(null)} disabled={!!busy}>Cancel</button><button className="primary" disabled={!approved || !!busy} onClick={() => task('Executing approved batch', async () => { const result = await api<Run>(`/runs/${run!.id}/execute`, 'POST', { digest: run!.digest, actionIds: approval.map(a => a.id), approved: true }); setRun(result); setSelected(result.actions.find(a => a.id === selected?.id) || null); setApproval(null); })}>{busy ? <LoaderCircle size={15} className="spin"/> : <Play size={15}/>}Approve & execute</button></div></section></div>}
  {adminOpen && <div className="modal-overlay"><section className="modal" role="dialog" aria-modal="true" aria-labelledby="admin-title"><button className="modal-close" onClick={() => { setAdminOpen(false); setPassword(''); }} aria-label="Close settings"><X /></button><span className="modal-icon"><LockKeyhole size={26}/></span>{config?.authenticated && <button className="text-button" disabled={!!busy} onClick={() => task('Signing out', async () => { await api('/logout', 'POST'); await refreshCsrf(); setConfig(await api<Config>('/config')); clearRun(); setHistory([]); setCandidates([]); setAdminOpen(false); })}>Sign out</button>}<h2 id="admin-title">{config?.admin ? 'Reference environment' : 'Administrator access'}</h2><p>{config?.admin ? 'Configure one reference base. The target segment selects its matching reference schema.' : 'Application accounts are configured on the server. Reference settings are restricted to administrators.'}</p>{!config?.admin ? <form onSubmit={e => { e.preventDefault(); task('Signing in', async () => { try {
        await login(username, password);
        const signedIn = await api<Config>('/config');
        setConfig(signedIn);
        clearRun();
        if (!signedIn.admin)
            setAdminOpen(false);
    }
    finally {
        setPassword('');
    } }); }}><label>Account<select value={username} onChange={e => setUsername(e.target.value)}><option value="admin">Administrator</option><option value="operator">Operator</option></select></label><label>Password<input type="password" autoComplete="current-password" value={password} onChange={e => setPassword(e.target.value)}/></label><button className="primary" disabled={!!busy || !password}>Sign in<ArrowRight size={15}/></button></form> : <><label>Reference base & server hint<div className="input-with-button"><input value={referenceInput} onChange={e => { setReferenceInput(e.target.value); setReferenceAlias(''); setReferenceAliases([]); }}/><button aria-label="Find reference TNS" disabled={!!busy} onClick={() => task('Finding reference TNS', async () => { const r = await api<{
        aliases: string[];
    }>('/tns?input=' + encodeURIComponent(referenceInput)); setReferenceAliases(r.aliases); })}><Search size={15}/></button></div></label><label>Reference TNS<select value={referenceAlias} onChange={e => setReferenceAlias(e.target.value)}><option value="">Select alias</option>{referenceAliases.map(a => <option key={a}>{a}</option>)}</select></label><label>Application release<input placeholder="e.g. release-2026.09" value={release} onChange={e => setRelease(e.target.value)}/></label><div className="modal-footer"><button className="secondary" disabled={!!busy || !config.referenceReady} onClick={() => task('Clearing reference', async () => { await api('/admin/reference', 'DELETE'); setConfig(await api<Config>('/config')); setAdminOpen(false); })}>Deactivate</button><button className="primary" disabled={!!busy || !referenceAlias || !release} onClick={() => task('Configuring reference', async () => { await api('/admin/reference', 'POST', { input: referenceInput, alias: referenceAlias, release }); setConfig(await api<Config>('/config')); setAdminOpen(false); setNotice('Reference configuration saved. Availability is checked during each lookup.'); })}>Save reference<Check size={15}/></button></div></>}{error && <div className="message error">{error}</div>}</section></div>}
  {referencePreview && <div className="modal-overlay"><section className="modal approval-modal" role="dialog" aria-modal="true" aria-label="Reference comparison"><button className="modal-close" onClick={() => setReferencePreview(null)} aria-label="Close comparison"><X /></button><h2>Reference comparison</h2><p>{referencePreview.sourceSchema} · {referencePreview.release}</p><pre>{referencePreview.sql}</pre><div className="message">{referencePreview.message}</div><label>Target application release<input value={targetRelease} onChange={e => setTargetRelease(e.target.value)} placeholder="Must match the configured reference release"/></label><label className="approval-check"><input type="checkbox" checked={compatible} onChange={e => setCompatible(e.target.checked)}/>I reviewed source compatibility and environment-specific references.</label><div className="modal-footer"><button className="secondary" onClick={() => { setReferencePreview(null); setCompatible(false); }}>Close preview</button><button className="primary" disabled={!!busy || historical || !compatible || targetRelease !== referencePreview.release || !selected || !['FAILED', 'MANUAL'].includes(selected.status)} onClick={() => task('Preparing reference repair', async () => { const result = await api<Run>(`/runs/${run!.id}/reference-plan`, 'POST', { actionId: selected!.id, release: targetRelease, checksum: referencePreview.checksum, compatibilityReviewed: compatible }); setRun(result); setSelected(result.actions.find(a => a.id === selected!.id) || null); setReferencePreview(null); setCompatible(false); setNotice('Reference repair added. Review and approve its exact script before execution.'); })}>Add to repair plan</button></div></section></div>}
 </div>;
}
function Status({ status }: {
    status: string;
}) { const labels: Record<string, string> = { PENDING: 'Pending', SUCCESS: 'Verified', FAILED: 'Failed', BLOCKED: 'Blocked', MANUAL: 'Review needed', SKIPPED: 'Skipped', RUNNING: 'Running', REVIEW: 'In review', COMPLETED: 'Completed', NEEDS_RESCAN: 'Needs re-scan' }; return <span className={'status status-' + status.toLowerCase()}><span />{labels[status] || status}</span>; }
function Metric({ label, value, icon, note, tone = '', small = false }: {
    label: string;
    value: string;
    icon: React.ReactNode;
    note: string;
    tone?: string;
    small?: boolean;
}) { return <div className={'metric card ' + tone}><div className="metric-label">{label}{icon}</div><strong className={small ? 'metric-small' : ''}>{value}</strong><span className="metric-note">{note}</span></div>; }
Enter fullscreen mode Exit fullscreen mode

frontend/src/api.ts

let csrf: {
    token: string;
    header: string;
} | null = null;
export async function refreshCsrf() { const res = await fetch('/api/csrf'); if (!res.ok)
    throw new Error('Unable to establish a secure session'); csrf = await res.json(); }
export async function api<T>(path: string, method = 'GET', body?: unknown): Promise<T> {
    if (method !== 'GET' && !csrf)
        await refreshCsrf();
    const headers: Record<string, string> = { 'Content-Type': 'application/json' };
    if (method !== 'GET' && csrf)
        headers[csrf.header] = csrf.token;
    const res = await fetch('/api' + path, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) });
    if (!res.ok) {
        const data = await res.json().catch(() => ({}));
        throw new Error(data.message || (res.status === 401 ? 'Please sign in to continue.' : res.status === 403 ? 'This action requires permission or a refreshed session.' : 'The operation could not complete.'));
    }
    return res.status === 204 ? undefined as T : res.json();
}
export async function login(username: string, password: string) {
    await refreshCsrf();
    const headers: Record<string, string> = { 'Content-Type': 'application/x-www-form-urlencoded' };
    headers[csrf!.header] = csrf!.token;
    const res = await fetch('/api/login', { method: 'POST', headers, body: new URLSearchParams({ username, password }) });
    if (!res.ok)
        throw new Error('Sign-in failed. Check the externally configured application account.');
    await refreshCsrf();
}
Enter fullscreen mode Exit fullscreen mode

frontend/src/main.tsx

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import './styles.css';
createRoot(document.getElementById('root')!).render(<React.StrictMode><App /></React.StrictMode>);
Enter fullscreen mode Exit fullscreen mode

frontend/src/styles.css

:root {
  font-family: Inter,"Segoe UI",Arial,sans-serif;
  color: #25352f;
  background: #f5f7f5;
  font-synthesis: none;
  font-weight: 400;
  font-size: 13px;
  --green: #245f48;
  --dark: #142c23;
  --border: #e3e9e4;
  --muted: #7b8981;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
}

button,input,select {
  font: inherit;
}

button {
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  transition: background .15s,border-color .15s;
}

button:disabled {
  opacity: .4;
  cursor: not-allowed;
}

button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,tr:focus-visible {
  outline: 2px solid #65a782;
  outline-offset: 3px;
}

button {
  border: 0;
}

input,select {
  min-width: 0;
  border: 1px solid #dce3dd;
  border-radius: 6px;
  background: #fff;
  color: #2f4037;
  height: 40px;
  padding: 0 11px;
  width: 100%;
  outline: none;
}

input:focus,select:focus {
  border-color: #599c76;
}

a {
  color: inherit;
  text-decoration: none;
}

h1,h2,h3,p {
  margin: 0;
}

h2 {
  font-size: 15px;
  font-weight: 650;
  letter-spacing: -.25px;
}

h3 {
  font-size: 14px;
}

code,pre {
  font-family: "Cascadia Code",Consolas,monospace;
}

.app {
  display: flex;
  min-height: 100vh;
}

.sidebar {
  background: #142b23;
  width: 228px;
  position: fixed;
  inset: 0 auto 0 0;
  color: #c2cfc6;
  display: flex;
  flex-direction: column;
  padding: 30px 16px 20px;
}

.brand {
  display: flex;
  gap: 10px;
  align-items: center;
  font-size: 21px;
  font-weight: 650;
  letter-spacing: -.8px;
  color: #f3f7f2;
  padding: 0 9px;
}

.brand-light {
  font-weight: 350;
  color: #c9dacb;
}

.brand small {
  display: block;
  font-size: 8px;
  letter-spacing: 2.5px;
  color: #91a496;
  margin-top: 5px;
}

.brand-mark {
  background: #2b4b3a;
  border: 1px solid #405e46;
  color: #c3e8b3;
  width: 37px;
  height: 39px;
  border-radius: 10px;
  display: grid;
  place-items: center;
}

.nav-label {
  font-size: 9px;
  font-weight: 650;
  letter-spacing: 1.7px;
  color: #799184;
  margin: 49px 14px 15px;
}

.nav-item {
  background: transparent;
  color: #acbdb1;
  justify-content: flex-start;
  width: 100%;
  padding: 13px 13px;
  border-radius: 6px;
  margin-bottom: 5px;
  font-size: 12px;
  gap: 12px;
  white-space: nowrap;
}

.nav-item.active {
  background: #2b4937;
  color: #e1f2da;
}

.nav-item:hover {
  background: #243f31;
}

.nav-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: #abd598;
  margin-left: auto;
}

.tools-label {
  margin-top: 31px;
}

.sidebar-bottom {
  margin-top: auto;
  padding: 30px 12px 0;
}

.connection-orbit {
  width: 47px;
  height: 47px;
  background: #203d2e;
  border: 1px solid #395443;
  border-radius: 13px;
  display: grid;
  place-items: center;
  color: #a2c491;
  position: relative;
  margin-bottom: 15px;
}

.connection-orbit span {
  position: absolute;
  right: -3px;
  bottom: 4px;
  width: 11px;
  height: 11px;
  background: #90c078;
  border: 3px solid #142b23;
  border-radius: 50%;
}

.sidebar-bottom strong {
  font-size: 11px;
  font-weight: 500;
  color: #c1d1c4;
}

.sidebar-bottom p {
  font-size: 10px;
  color: #839b8b;
  line-height: 1.9;
  margin-top: 8px;
}

.sidebar-version {
  border-top: 1px solid #30483a;
  margin-top: 26px;
  padding-top: 19px;
  font-size: 8px;
  letter-spacing: 1px;
  display: flex;
  align-items: center;
  gap: 7px;
  color: #7c9686;
}

.sidebar-version>span:last-child {
  margin-left: auto;
}

.tiny-dot {
  width: 6px;
  height: 6px;
  display: inline-block;
  border-radius: 50%;
  background: #8bac7d;
  flex-shrink: 0;
}

.tiny-dot.amber {
  background: #c7a268;
}

.tiny-dot.green {
  background: #488863;
}

.main-shell {
  margin-left: 228px;
  flex: 1;
  min-width: 0;
}

.topbar {
  height: 70px;
  border-bottom: 1px solid var(--border);
  background: #fff;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 35px;
}

.breadcrumbs {
  display: flex;
  align-items: center;
  gap: 13px;
  font-size: 11px;
  color: #8b958f;
}

.breadcrumbs span {
  color: #394c41;
}

.topbar-right {
  display: flex;
  align-items: center;
  gap: 21px;
}

.reference-status {
  background: transparent;
  color: #748174;
  font-size: 10px;
  gap: 8px;
}

.top-divider {
  height: 22px;
  border-right: 1px solid var(--border);
}

.avatar {
  border: 1px solid #dfe5df;
  color: #5d725f;
  background: #f0f3ed;
  border-radius: 50%;
  width: 30px;
  height: 30px;
  font-size: 10px;
  font-weight: 600;
}

main {
  padding: 32px 35px 18px;
  max-width: 1650px;
  margin: auto;
}

.page-heading {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 25px;
}

.eyebrow {
  font-size: 8px;
  font-weight: 650;
  letter-spacing: 1.8px;
  color: #7b8e7d;
  margin-bottom: 9px;
}

h1 {
  font-weight: 550;
  font-size: 29px;
  letter-spacing: -1px;
  line-height: 1.2;
}

.page-heading p {
  font-size: 11px;
  color: #839086;
  margin-top: 9px;
}

.mode-badge {
  font-size: 8px;
  letter-spacing: 1.2px;
  border: 1px solid #d8e2d6;
  border-radius: 5px;
  padding: 8px 10px;
  color: #63805e;
  display: flex;
  align-items: center;
  gap: 7px;
  background: #f0f5ed;
}

.demo-banner {
  padding: 12px 15px;
  background: #edf3e9;
  border: 1px solid #dce7d7;
  border-radius: 6px;
  display: flex;
  align-items: center;
  gap: 10px;
  color: #6c8065;
  font-size: 10px;
  margin-bottom: 23px;
}

.demo-banner strong {
  color: #4f684a;
  font-weight: 600;
}

.banner-tag {
  margin-left: auto;
  font-size: 7px;
  letter-spacing: 1.4px;
  color: #809679;
  white-space: nowrap;
}

.card {
  border: 1px solid var(--border);
  border-radius: 9px;
  background: white;
  box-shadow: 0 2px 3px #142b2302;
}

.connection-card {
  padding: 20px 22px 0;
}

.section-title {
  display: flex;
  align-items: center;
  gap: 11px;
}

.section-title p,.plan-header p {
  font-size: 10px;
  color: #8c978f;
  margin-top: 5px;
}

.section-icon {
  width: 34px;
  height: 34px;
  border: 1px solid #e3e9df;
  background: #f5f8f2;
  border-radius: 7px;
  display: grid;
  place-items: center;
  color: #617e57;
}

.step-tag {
  margin-left: auto;
  font-size: 8px;
  letter-spacing: 1.4px;
  color: #9aa69b;
  font-weight: 600;
}

.connection-fields {
  display: grid;
  grid-template-columns: 1.1fr 1fr 1.25fr auto;
  gap: 17px;
  align-items: end;
  margin: 23px 0 20px;
}

label {
  font-size: 10px;
  font-weight: 550;
  color: #6c7d70;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.input-with-button {
  display: flex;
  border: 1px solid #dce3dd;
  border-radius: 6px;
  overflow: hidden;
  background: white;
}

.input-with-button input {
  border: none;
  min-width: 0;
}

.input-with-button button {
  width: 36px;
  flex-shrink: 0;
  background: #fff;
  color: #8ca18f;
}

.segment-buttons {
  display: flex;
  height: 40px;
  gap: 3px;
  border: 1px solid #e1e6df;
  border-radius: 6px;
  padding: 4px;
  background: #f9faf8;
}

.segment-buttons button {
  background: transparent;
  color: #7e8b7c;
  font-size: 10px;
  padding: 0 9px;
  border-radius: 3px;
  flex: 1;
}

.segment-buttons button.chosen {
  background: #e4eddf;
  color: #467242;
  box-shadow: 0 1px 2px #20302415;
  font-weight: 650;
}

.primary {
  background: #286044;
  color: #fff;
  font-size: 10px;
  font-weight: 550;
  padding: 11px 14px;
  border-radius: 5px;
  white-space: nowrap;
  min-height: 36px;
}

.primary:hover:not(:disabled) {
  background: #1b4931;
}

.connect-button {
  height: 40px;
}

.connection-footer {
  border-top: 1px solid #edf0eb;
  padding: 12px 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  color: #8c988e;
  font-size: 9px;
}

.connection-footer span {
  display: flex;
  gap: 8px;
  align-items: center;
}

.connection-footer code {
  font-size: 9px;
  color: #5d775b;
  background: #f3f6ef;
  padding: 3px 7px;
  border-radius: 3px;
}

.metrics {
  display: grid;
  grid-template-columns: repeat(4,1fr);
  gap: 15px;
  margin: 20px 0;
}

.metric {
  padding: 17px 18px 16px;
}

.metric-label {
  font-size: 10px;
  font-weight: 500;
  color: #768577;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.metric-label svg {
  color: #94a58e;
}

.metric strong {
  display: block;
  font-size: 29px;
  font-weight: 550;
  letter-spacing: -1px;
  margin: 12px 0 7px;
}

.metric.orange strong {
  color: #ad7944;
}

.metric.green strong {
  color: #4b7654;
}

.metric strong.metric-small {
  font-size: 22px;
  margin-top: 17px;
  margin-bottom: 10px;
  color: #647261;
  letter-spacing: -.6px;
}

.metric-note {
  font-size: 9px;
  color: #93a08f;
}

.workspace-grid {
  display: grid;
  grid-template-columns: minmax(0,1fr) 292px;
  gap: 19px;
  align-items: start;
}

.plan-card {
  overflow: hidden;
}

.plan-header {
  padding: 21px 21px 18px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.plan-header h2 {
  display: flex;
  gap: 8px;
  align-items: center;
}

.count-pill {
  font-size: 9px;
  border: 1px solid #e1e7df;
  background: #f5f7f2;
  padding: 2px 6px;
  border-radius: 4px;
  color: #72856c;
}

.plan-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-top: 1px solid #f0f2ed;
  border-bottom: 1px solid #e8ece5;
  padding: 11px 18px;
  gap: 10px;
}

.search-field {
  display: flex;
  align-items: center;
  gap: 7px;
  color: #9aa697;
}

.search-field input {
  height: 25px;
  border: 0;
  width: 160px;
  padding: 0;
  font-size: 10px;
}

.quiet-text {
  font-size: 9px;
  color: #929e91;
}

.table-scroll {
  overflow: auto;
}

table {
  width: 100%;
  border-collapse: collapse;
  text-align: left;
}

th {
  font-size: 7px;
  font-weight: 650;
  letter-spacing: 1px;
  color: #929e8e;
  background: #fafbf8;
  padding: 12px 12px;
  border-bottom: 1px solid #e8ede4;
  white-space: nowrap;
}

th:first-child,td:first-child {
  padding-left: 21px;
}

td {
  padding: 15px 12px;
  border-bottom: 1px solid #edf0e9;
  font-size: 11px;
  color: #52664d;
}

tbody tr {
  cursor: pointer;
}

tbody tr:hover {
  background: #f8faf5;
}

tbody tr.selected {
  background: #f1f6ec;
  box-shadow: inset 3px 0 #83a671;
}

td strong {
  display: block;
  font-family: "Cascadia Code",Consolas,monospace;
  font-size: 10px;
  font-weight: 550;
  color: #42523d;
}

td small {
  display: block;
  font-size: 8px;
  color: #93a18d;
  margin-top: 5px;
  white-space: nowrap;
}

td small span {
  padding: 0 3px;
}

.rank {
  display: grid;
  place-items: center;
  border: 1px solid #e2e8da;
  background: #fafbf7;
  width: 25px;
  height: 25px;
  border-radius: 6px;
  font-size: 10px;
  color: #7b8b6e;
  font-family: Consolas,monospace;
}

.rank-done {
  background: #e1eed9;
  color: #57864b;
  border-color: #d8e7ce;
}

.source-tag {
  font-size: 8px;
  display: flex;
  align-items: center;
  gap: 5px;
  color: #85977c;
  white-space: nowrap;
}

.status {
  font-size: 8px;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 6px;
  background: #f3f3ed;
  border: 1px solid #e8e9e0;
  color: #8f947d;
  border-radius: 4px;
  white-space: nowrap;
}

.status>span {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: currentColor;
}

.status-success,.status-completed {
  background: #eef5e8;
  color: #60894e;
  border-color: #dbe8d1;
}

.status-failed,.status-blocked,.status-needs_rescan {
  background: #fbf0e9;
  border-color: #efdccc;
  color: #aa7556;
}

.status-manual {
  background: #fbf5e9;
  border-color: #efe3c9;
  color: #a78b50;
}

.status-running {
  color: #567f99;
  background: #eff5fa;
}

.plan-bottom {
  padding: 15px 18px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  background: #fcfdf9;
  gap: 10px;
}

.plan-bottom>div {
  display: flex;
  gap: 7px;
  align-items: center;
  color: #899780;
  font-size: 9px;
}

.detail-card {
  padding: 20px 19px;
}

.detail-heading {
  display: flex;
  justify-content: space-between;
  align-items: center;
  color: #859779;
}

.detail-heading h2 {
  font-size: 13px;
  color: #4a5e43;
}

.object-title {
  padding-top: 23px;
}

.object-type {
  font-size: 8px;
  letter-spacing: 1.2px;
  color: #97a28d;
}

.object-title h3 {
  font-size: 14px;
  font-family: Consolas,monospace;
  margin: 8px 0 11px;
  color: #41543b;
  overflow-wrap: anywhere;
}

.detail-card dl {
  border-top: 1px solid #ebeee6;
  border-bottom: 1px solid #ebeee6;
  padding: 14px 0;
  margin: 17px 0 19px;
  display: grid;
  gap: 11px;
}

.detail-card dl div {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  font-size: 9px;
}

.detail-card dt {
  color: #98a28f;
}

.detail-card dd {
  margin: 0;
  color: #5d7152;
  text-align: right;
  word-break: break-word;
}

.detail-card h4 {
  font-size: 7px;
  letter-spacing: 1px;
  color: #8f9e83;
  margin: 17px 0 10px;
}

.detail-card h4 span {
  float: right;
  font-size: 6px;
  color: #b0b9a9;
}

.reason {
  font-size: 10px;
  line-height: 1.7;
  color: #7b8b71;
}

pre {
  font-size: 9px;
  line-height: 1.8;
  padding: 14px;
  background: #f6f8f2;
  border: 1px solid #e8ede0;
  border-radius: 5px;
  overflow: auto;
  color: #63794f;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  margin: 0;
  max-height: 260px;
}

.dependency-note {
  display: flex;
  gap: 6px;
  font-size: 8px;
  line-height: 1.5;
  color: #97a388;
  margin-top: 12px;
}

.detail-actions {
  display: flex;
  flex-direction: column;
  gap: 9px;
  margin-top: 18px;
}

.secondary {
  border: 1px solid #dce5d3;
  border-radius: 5px;
  color: #647d50;
  background: #fafcf7;
  font-size: 10px;
  padding: 10px 12px;
  min-height: 35px;
}

.secondary:hover:not(:disabled) {
  background: #eef4e7;
}

.text-button {
  background: transparent;
  color: #758f60;
  font-size: 9px;
  padding: 5px;
  gap: 7px;
}

.text-button.muted {
  color: #a0ab94;
}

.activity-card {
  margin-top: 20px;
  overflow: hidden;
}

.activity-heading {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 17px 20px;
  border-bottom: 1px solid #e9ede3;
}

.activity-heading>div {
  display: flex;
  align-items: center;
  gap: 10px;
  color: #889c78;
}

.activity-heading h2 {
  font-size: 12px;
  color: #506344;
}

.activity-heading .quiet-text {
  margin-left: 7px;
  font-family: Consolas,monospace;
  font-size: 8px;
}

.activity-empty {
  padding: 22px;
  color: #9aa88d;
  font-family: Consolas,monospace;
  font-size: 10px;
  display: flex;
  align-items: center;
  gap: 11px;
}

.events {
  max-height: 215px;
  overflow: auto;
  padding: 13px 20px;
}

.event {
  display: flex;
  align-items: baseline;
  gap: 11px;
  padding: 7px 0;
  font-size: 9px;
  line-height: 1.4;
}

.event time {
  font-family: Consolas,monospace;
  color: #a1ad94;
  white-space: nowrap;
}

.event strong {
  color: #6d8558;
  font-weight: 500;
  white-space: nowrap;
}

.event>span:last-of-type {
  color: #8e9e7c;
}

.event small {
  margin-left: auto;
  color: #a4b494;
}

.event-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: #8aa56e;
  flex-shrink: 0;
}

.event-dot.failed {
  background: #b68562;
}

footer {
  display: flex;
  justify-content: space-between;
  font-size: 8px;
  color: #a1ad98;
  margin-top: 23px;
}

footer span {
  display: flex;
  align-items: center;
  gap: 6px;
}

.empty-plan {
  text-align: center;
  padding: 46px 15px;
  color: #92a286;
}

.empty-icon {
  width: 58px;
  height: 58px;
  margin: 0 auto 18px;
  background: #f0f5e9;
  border: 1px solid #e3ebdb;
  border-radius: 16px;
  display: grid;
  place-items: center;
  color: #91aa7d;
}

.empty-plan h3 {
  font-weight: 500;
  color: #627951;
  font-size: 13px;
  margin: 10px 0;
}

.empty-plan p {
  font-size: 10px;
  line-height: 1.9;
  color: #98a68e;
}

.empty-flow {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 12px;
  margin-top: 25px;
  font-size: 8px;
  color: #a0ae95;
}

.empty-detail {
  padding: 60px 8px;
  text-align: center;
  font-size: 11px;
  line-height: 1.8;
  color: #9fab92;
}

.empty-detail p {
  margin-top: 12px;
}

.message {
  padding: 12px 15px;
  background: #edf3e7;
  border: 1px solid #dbe5d1;
  border-radius: 6px;
  color: #70825f;
  margin-bottom: 16px;
  line-height: 1.6;
  font-size: 11px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.message.error {
  background: #fcf0e9;
  color: #9e644b;
  border-color: #f0d9ca;
}

.message button {
  background: transparent;
  color: inherit;
}

.busy-line {
  font-size: 10px;
  color: #72905c;
  margin-bottom: 12px;
  display: flex;
  gap: 7px;
  align-items: center;
}

.spin {
  animation: spin 1s linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

.modal-overlay {
  position: fixed;
  inset: 0;
  background: #142b2370;
  backdrop-filter: blur(3px);
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.modal {
  width: 440px;
  background: #fff;
  border: 1px solid #e6eddf;
  border-radius: 12px;
  padding: 30px;
  box-shadow: 0 25px 100px #142b2330;
  position: relative;
  max-height: 90vh;
  overflow: auto;
}

.modal h2 {
  font-size: 22px;
  font-weight: 550;
  margin: 10px 0;
}

.modal>p {
  color: #859777;
  font-size: 11px;
  line-height: 1.8;
  margin-bottom: 23px;
}

.modal-close {
  position: absolute;
  top: 14px;
  right: 14px;
  background: transparent;
  color: #95a788;
  padding: 5px;
}

.modal-close svg {
  width: 18px;
}

.modal-icon {
  display: grid;
  place-items: center;
  color: #73925b;
  background: #eef5e7;
  border: 1px solid #e0ebd6;
  border-radius: 11px;
  width: 49px;
  height: 49px;
  margin-bottom: 20px;
}

.modal label {
  margin: 15px 0;
}

.modal .primary {
  margin-top: 5px;
}

.modal-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 20px;
  align-items: center;
}

.approval-modal {
  width: 620px;
}

.approval-script {
  margin: 15px 0;
  border: 1px solid #e3eadb;
  border-radius: 7px;
  padding: 13px;
}

.approval-script>div {
  display: flex;
  justify-content: space-between;
  margin-bottom: 10px;
  font-size: 10px;
  color: #71875e;
}

.approval-script>div code {
  font-size: 9px;
}

.approval-script small {
  display: block;
  margin-top: 8px;
  font-size: 8px;
  color: #8d9f7b;
}

.approval-check {
  flex-direction: row;
  align-items: center;
  line-height: 1.5;
}

.approval-check input {
  height: 16px;
  width: 16px;
  accent-color: #47753b;
}

.history-card .section-title {
  padding: 23px;
}

.history-card .section-title .quiet-text {
  margin-left: auto;
}

.history-card {
  overflow: auto;
}

.library-info {
  padding: 35px;
  color: #708961;
}

.library-info h2 {
  margin-top: 18px;
  font-size: 22px;
}

.library-info>p {
  margin-top: 13px;
  max-width: 660px;
  font-size: 12px;
  line-height: 1.9;
}

.library-steps {
  display: grid;
  grid-template-columns: repeat(3,1fr);
  gap: 25px;
  margin: 35px 0;
}

.library-steps>div {
  border: 1px solid #e4eadc;
  border-radius: 8px;
  padding: 22px;
}

.library-steps span {
  font: 22px Consolas,monospace;
  color: #a6b695;
}

.library-steps h3 {
  margin: 18px 0 10px;
}

.library-steps p {
  font-size: 11px;
  line-height: 1.8;
  color: #91a181;
}

@media (min-width:1500px) {
  .workspace-grid {
    grid-template-columns: minmax(0,1fr) 330px;
  }
  td {
    padding-top: 18px;
    padding-bottom: 18px;
  }
}

@media (max-width:1200px) {
  .sidebar {
    width: 194px;
    padding-left: 10px;
    padding-right: 10px;
  }
  .brand {
    font-size: 18px;
  }
  .main-shell {
    margin-left: 194px;
  }
  main {
    padding: 27px 23px;
  }
  .topbar {
    padding: 0 23px;
  }
  .connection-fields {
    grid-template-columns: 1fr 1fr;
    gap: 14px;
  }
  .workspace-grid {
    grid-template-columns: minmax(0,1fr) 258px;
  }
  .metric {
    padding: 15px 13px;
  }
  .metrics {
    gap: 10px;
  }
  .metric-note {
    font-size: 8px;
  }
  .plan-bottom {
    flex-wrap: wrap;
  }
  .source-tag {
    font-size: 7px;
  }
  td,th {
    padding-left: 9px;
    padding-right: 9px;
  }
}

@media (max-width:950px) {
  .sidebar {
    width: 66px;
    padding: 25px 8px;
  }
  .brand {
    padding: 0;
    justify-content: center;
  }
  .brand>span:last-child,.nav-label,.sidebar-bottom,.nav-item .nav-dot {
    display: none;
  }
  .nav-item {
    font-size: 0;
    gap: 0;
    justify-content: center;
    padding: 13px 0;
  }
  .nav-item:first-of-type {
    margin-top: 42px;
  }
  .nav-item>svg:last-child:not(:first-child) {
    display: none;
  }
  .main-shell {
    margin-left: 66px;
  }
  .workspace-grid {
    grid-template-columns: 1fr;
  }
  .detail-card {
    display: block;
  }
  .metrics {
    grid-template-columns: repeat(2,1fr);
  }
  .plan-bottom {
    flex-wrap: nowrap;
  }
  .banner-tag {
    display: none;
  }
}

@media (max-width:600px) {
  .sidebar {
    display: none;
  }
  .main-shell {
    margin-left: 0;
  }
  main {
    padding: 22px 14px;
  }
  .topbar {
    height: 58px;
    padding: 0 14px;
  }
  .breadcrumbs {
    font-size: 9px;
  }
  .reference-status {
    font-size: 8px;
  }
  .topbar-right {
    gap: 9px;
  }
  .top-divider {
    display: none;
  }
  .page-heading {
    align-items: flex-start;
    gap: 10px;
  }
  h1 {
    font-size: 24px;
  }
  .page-heading p {
    font-size: 10px;
    line-height: 1.7;
  }
  .mode-badge {
    font-size: 6px;
    white-space: nowrap;
    padding: 7px;
    margin-top: 8px;
  }
  .connection-card {
    padding-left: 15px;
    padding-right: 15px;
  }
  .connection-fields {
    grid-template-columns: 1fr;
  }
  .connection-footer>span:last-child {
    display: none;
  }
  .step-tag {
    font-size: 6px;
  }
  .plan-bottom {
    flex-direction: column;
    align-items: stretch;
  }
  .plan-bottom>div {
    font-size: 8px;
  }
  .event {
    flex-wrap: wrap;
    gap: 7px;
  }
  .event>span:last-of-type {
    flex-basis: 100%;
    padding-left: 10px;
  }
  footer>span:last-child {
    display: none;
  }
  .library-steps {
    grid-template-columns: 1fr;
  }
  .modal {
    padding: 23px;
  }
  .demo-banner {
    font-size: 9px;
    line-height: 1.6;
  }
  .approval-script>div {
    flex-wrap: wrap;
    gap: 7px;
  }
  .metrics {
    gap: 9px;
  }
}

.field-label {
  display: block;
  font-size: 10px;
  font-weight: 550;
  color: #6c7d70;
  margin-bottom: 8px;
}

.candidate-list {
  margin: 24px 0;
}

.candidate-list>p {
  margin: 12px 0;
  color: #829373;
  font-size: 11px;
}
Enter fullscreen mode Exit fullscreen mode

frontend/src/types.ts

export type Config = {
    mode: string;
    segments: string[];
    referenceReady: boolean;
    writesEnabled: boolean;
    admin: boolean;
    authenticated: boolean;
};
export type Action = {
    id: string;
    objectName: string;
    objectType: string;
    rank: number;
    dependencies: string[];
    executionSchema: string;
    executionSegment: string;
    source: string;
    reason: string;
    sql: string;
    verification: string;
    status: string;
    message: string;
};
export type ObjectInfo = {
    name: string;
    type: string;
    status: string;
    errors: string[];
    dependencies: string[];
};
export type Run = {
    id: string;
    targetSchema: string;
    segment: string;
    alias: string;
    mode: string;
    status: string;
    createdAt: string;
    initialInvalid: number;
    remainingInvalid: number;
    digest: string;
    objects: ObjectInfo[];
    actions: Action[];
    events: {
        time: string;
        action: string;
        status: string;
        message: string;
        durationMs: number;
    }[];
};
export function nextBatch(actions: Action[]): Action[] {
    const done = new Set(actions.filter(a => a.status === 'SUCCESS').map(a => a.id));
    const ready = actions.filter(a => a.status === 'PENDING' && a.dependencies.every(d => done.has(d)));
    if (!ready.length)
        return [];
    const rank = Math.min(...ready.map(a => a.rank));
    // Each dependency is independently reviewed after it verifies, even within the same rank.
    return ready.filter(a => a.rank === rank);
}
Enter fullscreen mode Exit fullscreen mode

frontend/tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": [
      "ES2022",
      "DOM",
      "DOM.Iterable"
    ],
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "jsx": "react-jsx",
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "src",
    "vite.config.ts"
  ]
}
Enter fullscreen mode Exit fullscreen mode

frontend/vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    host: '127.0.0.1',
    proxy: { '/api': 'http://127.0.0.1:8080' },
  },
});
Enter fullscreen mode Exit fullscreen mode

Top comments (0)