Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 104 additions & 23 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,44 @@ export default function App() {
const rpcOk = health?.rpc_ok ?? false
const apiOk = health !== null
const isReadOnly = networkMode?.read_only ?? false
const dashboardCopy =
lang === 'pt-BR'
? {
tag: 'btcneves@nodescope:~$ inspect bitcoin-core',
title: 'Bitcoin Core Professional Lab',
subtitle:
'Laboratorio visual, guiado e auditavel para observar RPC, ZMQ, mempool, blocos e transacoes em tempo real.',
primary: 'Executar demo guiada',
secondary: 'Inspecionar TXID',
tertiary: 'Ver fita ZMQ',
consoleTitle: 'live node console',
visualTitle: 'observability matrix',
proof: 'Proof Report',
modules: [
['RPC real', 'health, blocks, tx decode', 'live'],
['ZMQ rawtx/rawblock', 'eventos validados por RPC', 'stream'],
['Mempool policy', 'RBF, CPFP e fee rate', 'lab'],
['Cluster mempool', 'detectado em runtime', 'fallback'],
],
}
: {
tag: 'btcneves@nodescope:~$ inspect bitcoin-core',
title: 'Bitcoin Core Professional Lab',
subtitle:
'A visual, guided and auditable lab for observing RPC, ZMQ, mempool, blocks and transactions in real time.',
primary: 'Run guided demo',
secondary: 'Inspect TXID',
tertiary: 'Open ZMQ tape',
consoleTitle: 'live node console',
visualTitle: 'observability matrix',
proof: 'Proof Report',
modules: [
['Real RPC', 'health, blocks, tx decode', 'live'],
['ZMQ rawtx/rawblock', 'events validated by RPC', 'stream'],
['Mempool policy', 'RBF, CPFP and fee rate', 'lab'],
['Cluster mempool', 'detected at runtime', 'fallback'],
],
}

const handleInspect = (txid: string) => {
setInspectorTxid(txid)
Expand Down Expand Up @@ -178,33 +216,13 @@ export default function App() {
<div className="app" style={{ height: '100vh', overflow: 'hidden' }}>
{header}
{readOnlyBanner}
<div
style={{
display: 'flex',
gap: '20px',
padding: '16px 24px',
height: 'calc(100vh - 56px)',
overflow: 'hidden',
maxWidth: '1300px',
margin: '0 auto',
width: '100%',
}}
>
<div className="guided-demo-workspace">
{/* Left: scrollable steps list */}
<div style={{ flex: 1, overflowY: 'auto', minWidth: 0, paddingRight: 4 }}>
<div className="guided-demo-main">
<GuidedDemo onStepsChange={setGuidedDemoSteps} readOnly={isReadOnly} />
</div>
{/* Right: fixed sidebar — lifecycle + explain */}
<div
style={{
width: '300px',
flexShrink: 0,
display: 'flex',
flexDirection: 'column',
gap: '12px',
overflowY: 'auto',
}}
>
<div className="guided-demo-sidebar">
<TransactionLifecycle
rpcOk={rpcOk}
zmqConnected={sseConnected}
Expand Down Expand Up @@ -306,6 +324,69 @@ export default function App() {
{header}
{readOnlyBanner}
<main className="main">
<section className="lab-hero" aria-label="NodeScope Bitcoin Core lab overview">
<div className="lab-hero-copy">
<div className="terminal-pill">~ {dashboardCopy.tag}|</div>
<h1>{dashboardCopy.title}</h1>
<p>{dashboardCopy.subtitle}</p>
<div className="lab-hero-actions">
<button className="lab-primary-btn" onClick={() => setActiveView('guided-demo')}>
{dashboardCopy.primary}
</button>
<button className="lab-secondary-btn" onClick={() => setActiveView('inspector')}>
{dashboardCopy.secondary}
</button>
<button className="lab-secondary-btn" onClick={() => setActiveView('zmq-tape')}>
{dashboardCopy.tertiary}
</button>
</div>
<div className="lab-signal-row">
<span className={apiOk ? 'signal-ok' : 'signal-error'}>API</span>
<span className={rpcOk ? 'signal-ok' : 'signal-error'}>RPC</span>
<span className={sseConnected ? 'signal-ok' : 'signal-warn'}>SSE/ZMQ</span>
<span>{network}</span>
</div>
</div>

<div className="lab-visual" aria-hidden="true">
<div className="lab-window">
<div className="lab-window-bar">
<span />
<span />
<span />
<strong>{dashboardCopy.consoleTitle}</strong>
</div>
<pre>{`bitcoin-cli -regtest getblockchaininfo
chain=${network}
rpc_ok=${String(rpcOk)}
blocks=${health?.blocks ?? 'loading'}

zmq: rawtx=${sseConnected ? 'subscribed' : 'waiting'}
mempool.txs=${mempool?.size ?? 0}
proof=${summary ? 'replayable' : 'collecting'}`}</pre>
</div>
<div className="lab-code-card lab-code-card-a">
<span>{dashboardCopy.proof}</span>
<code>txid + vbytes + fee_rate</code>
</div>
<div className="lab-code-card lab-code-card-b">
<span>{dashboardCopy.visualTitle}</span>
<code>{'RPC -> ZMQ -> mempool -> block'}</code>
</div>
</div>
</section>

<section className="lab-module-grid" aria-label="NodeScope capability map">
{dashboardCopy.modules.map(([title, body, status]) => (
<article className="lab-module" key={title}>
<div className="module-icon">&lt;/&gt;</div>
<h2>{title}</h2>
<p>{body}</p>
<span>{status}</span>
</article>
))}
</section>

<ExplainBox text={t.explain.dashboard} />
<AlertingPanel readOnly={isReadOnly} />
<KpiRow summary={summary} mempool={mempool} health={health} />
Expand Down
28 changes: 5 additions & 23 deletions frontend/src/components/ClassificationsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,9 @@ export function ClassificationsTable({
</span>
</div>
<div className="panel-body">
<div style={{ display: 'flex', gap: 8, marginBottom: 8, fontSize: 11 }}>
<div className="event-sortbar">
{['ts', 'kind', 'confidence', 'inputs', 'outputs', 'total_out'].map((field) => (
<button
key={field}
onClick={() => onSort?.(field)}
style={{
background: 'transparent',
border: '1px solid #374151',
borderRadius: 4,
color: '#9ca3af',
cursor: 'pointer',
fontFamily: 'monospace',
}}
>
<button key={field} onClick={() => onSort?.(field)}>
{field} {sortLabel(field)}
</button>
))}
Expand Down Expand Up @@ -110,7 +99,9 @@ export function ClassificationsTable({
<span className="copy-feedback">{t.dashboard.copied}</span>
)}
<span
className={reason ? 'copyable-text' : undefined}
className={
reason ? 'classification-reason copyable-text' : 'classification-reason'
}
title={reason ? `${t.dashboard.copyReason}: ${reason}` : undefined}
onClick={() => void copyValue(reasonKey, reason)}
role={reason ? 'button' : undefined}
Expand All @@ -121,15 +112,6 @@ export function ClassificationsTable({
void copyValue(reasonKey, reason)
}
}}
style={{
fontSize: '11px',
color: 'var(--muted)',
marginLeft: 'auto',
maxWidth: '200px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{reason ?? ''}
</span>
Expand Down
53 changes: 7 additions & 46 deletions frontend/src/components/ClusterMempoolPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,35 +33,10 @@ export function ClusterMempoolPanel() {
}, [fetchData])

return (
<div
style={{
background: '#111827',
border: '1px solid #1f2937',
borderRadius: '8px',
padding: '16px',
fontFamily: 'monospace',
color: '#e5e7eb',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '12px',
}}
>
<div className="cluster-panel panel">
<div className="cluster-header">
<div>
<div
style={{
fontSize: '13px',
fontWeight: 'bold',
color: '#f9fafb',
display: 'flex',
alignItems: 'center',
gap: 6,
}}
>
<div className="cluster-title">
<Term term="Cluster mempool">{t.cluster.title}</Term>
</div>
<div style={{ fontSize: '11px', color: '#6b7280', marginTop: '2px' }}>
Expand All @@ -73,15 +48,7 @@ export function ClusterMempoolPanel() {
void fetchData()
}}
disabled={loading}
style={{
padding: '4px 12px',
fontSize: '11px',
borderRadius: '4px',
cursor: 'pointer',
background: 'transparent',
color: '#9ca3af',
border: '1px solid #374151',
}}
className="cluster-refresh-btn"
>
{loading ? '…' : t.cluster.refresh}
</button>
Expand Down Expand Up @@ -126,7 +93,7 @@ export function ClusterMempoolPanel() {
{t.cluster.fallback}
</div>
)}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
<div className="cluster-map">
{clusters.clusters.flatMap((cluster) =>
cluster.txs.map((tx) => {
const fee = tx.fee_rate_sat_vb
Expand All @@ -138,18 +105,12 @@ export function ClusterMempoolPanel() {
title={`${tx.txid} · ${fee} sat/vB`}
style={{
width: size,
height: 38,
background: bg,
border: '1px solid #374151',
borderRadius: 4,
padding: 5,
overflow: 'hidden',
fontSize: 10,
color: '#e5e7eb',
}}
className="cluster-tx-node"
>
<div>{fee} sat/vB</div>
<div style={{ color: '#9ca3af' }}>{tx.txid.slice(0, 10)}…</div>
<div>{tx.txid.slice(0, 10)}…</div>
</div>
)
})
Expand Down
15 changes: 2 additions & 13 deletions frontend/src/components/EventsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,9 @@ export function EventsTable({ events, sortBy = 'ts', sortDir = 'desc', onSort }:
</span>
</div>
<div className="panel-body">
<div style={{ display: 'flex', gap: 8, marginBottom: 8, fontSize: 11 }}>
<div className="event-sortbar">
{['ts', 'event', 'origin'].map((field) => (
<button
key={field}
onClick={() => onSort?.(field)}
style={{
background: 'transparent',
border: '1px solid #374151',
borderRadius: 4,
color: '#9ca3af',
cursor: 'pointer',
fontFamily: 'monospace',
}}
>
<button key={field} onClick={() => onSort?.(field)}>
{field} {sortLabel(field)}
</button>
))}
Expand Down
41 changes: 6 additions & 35 deletions frontend/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,26 +61,19 @@ export function Header({
return (
<header className="header">
<div className="header-brand">
<span className="header-mark" aria-hidden="true">
&lt;n&gt;
</span>
<span className="header-title">{t.header.title}</span>
<span className={`badge ${networkClass}`}>{network}</span>
</div>

{/* Nav tabs */}
<div className="header-nav">
{NAV.map(({ id, label }) => (
<button
key={id}
className={activeView === id ? 'is-active' : undefined}
onClick={() => onSetView(id)}
style={{
padding: '4px 12px',
fontSize: '12px',
background: activeView === id ? '#1d4ed8' : 'transparent',
color: activeView === id ? '#fff' : '#9ca3af',
border: activeView === id ? '1px solid #3b82f6' : '1px solid #374151',
borderRadius: '4px',
cursor: 'pointer',
fontFamily: 'monospace',
}}
>
{label}
</button>
Expand All @@ -94,18 +87,8 @@ export function Header({
<button
key={value}
onClick={() => setLang(value)}
className={lang === value ? 'is-active' : undefined}
title={value}
style={{
padding: '2px 7px',
fontSize: '11px',
background: lang === value ? '#1d4ed8' : 'transparent',
color: lang === value ? '#fff' : '#6b7280',
border: lang === value ? '1px solid #3b82f6' : '1px solid #374151',
borderRadius: '3px',
cursor: 'pointer',
fontFamily: 'monospace',
fontWeight: lang === value ? 700 : 400,
}}
>
{label}
</button>
Expand All @@ -128,19 +111,7 @@ export function Header({
{t.actions.refresh}
</button>
{onNewSession && (
<button
onClick={onNewSession}
style={{
padding: '4px 10px',
fontSize: '11px',
background: 'transparent',
color: '#6b7280',
border: '1px solid #374151',
borderRadius: '4px',
cursor: 'pointer',
fontFamily: 'monospace',
}}
>
<button onClick={onNewSession} className="session-btn">
{t.header.newSession}
</button>
)}
Expand Down
Loading
Loading