diff --git a/web/src/app.js b/web/src/app.js index bbb02cb..b413433 100644 --- a/web/src/app.js +++ b/web/src/app.js @@ -192,7 +192,13 @@ import { } catch (err) { const userErr = toUserError(err, "REST"); setLiveError(userErr); - setMode("demo", `${userErr} - running in demo mode`); + if (state.mode === "live") { + setWarning(`${userErr} - staying in live mode`); + state.live.connected = false; + setModeBadge(); + } else { + setMode("demo", `${userErr} - running in demo mode`); + } } } @@ -272,7 +278,13 @@ import { es.onerror = () => { const errText = `SSE: network/CORS blocked (${state.live.beaconUrl})`; setLiveError(errText); - setMode("demo", `${errText} - running in demo mode`); + if (state.mode === "live") { + setWarning(`${errText} - staying in live mode`); + state.live.connected = false; + setModeBadge(); + } else { + setMode("demo", `${errText} - running in demo mode`); + } }; const bindEvent = (name) => { @@ -288,7 +300,13 @@ import { } catch (err) { const userErr = toUserError(err, "SSE"); setLiveError(userErr); - setMode("demo", `${userErr} - running in demo mode`); + if (state.mode === "live") { + setWarning(`${userErr} - staying in live mode`); + state.live.connected = false; + setModeBadge(); + } else { + setMode("demo", `${userErr} - running in demo mode`); + } return; } @@ -303,8 +321,15 @@ import { if (state.mode !== "live") return; const idleFor = Date.now() - state.live.lastEventAt; if (idleFor > 20000) { - setLiveError("SSE: timeout waiting for events"); - setMode("demo", "Live SSE timeout - running in demo mode"); + const errText = "SSE: timeout waiting for events"; + setLiveError(errText); + if (state.mode === "live") { + setWarning(`${errText} - staying in live mode`); + state.live.connected = false; + setModeBadge(); + } else { + setMode("demo", "Live SSE timeout - running in demo mode"); + } } }, 5000); } @@ -336,7 +361,7 @@ import { nodes.settingsPanel.classList.toggle("open"); }); - nodes.applySettings.addEventListener("click", () => { + const applySettingsFromInputs = () => { state.live.beaconUrl = nodes.beaconInput.value.trim() || "http://localhost:5052"; state.live.metricsUrl = nodes.metricsInput.value.trim() || "http://localhost:9090"; state.live.apiNamespace = nodes.nsSelect.value === "eth" ? "eth" : "lean"; @@ -346,6 +371,10 @@ import { const mode = nodes.modeSelect.value === "live" ? "live" : "demo"; setMode(mode); + }; + + nodes.applySettings.addEventListener("click", () => { + applySettingsFromInputs(); }); } diff --git a/web/src/render/chain.js b/web/src/render/chain.js index 1f60ef0..c1039bb 100644 --- a/web/src/render/chain.js +++ b/web/src/render/chain.js @@ -44,6 +44,11 @@ export function drawChainCanvas(now) { visible.sort((a, b) => a.block.slot - b.block.slot); + ctx.save(); + ctx.beginPath(); + ctx.rect(0, 0, w, h); + ctx.clip(); + for (let i = 1; i < visible.length; i++) { const prev = visible[i - 1]; const cur = visible[i]; @@ -60,8 +65,10 @@ export function drawChainCanvas(now) { for (const item of visible) { const { block, x, y } = item; + if (x < 2 || (x + blockW) > (w - 2)) continue; const status = getBlockStatus(block); const fade = Math.max(0.1, Math.min(1, (x + 80) / (w * 0.52))); + const edgeGlowClamp = x < 8 || (x + blockW) > (w - 8); let color = COLORS.blue; if (status === "JUSTIFIED") color = COLORS.amber; @@ -78,10 +85,10 @@ export function drawChainCanvas(now) { ctx.stroke(); ctx.setLineDash([]); - ctx.shadowBlur = status === "MISSED" ? 0 : 18; + ctx.shadowBlur = (status === "MISSED" || edgeGlowClamp) ? 0 : 18; ctx.shadowColor = status === "FINALIZED" ? "rgba(34,197,94,0.65)" : `${color}99`; if (block.flashUntil > now) { - ctx.shadowBlur = 30; + ctx.shadowBlur = edgeGlowClamp ? 0 : 30; ctx.shadowColor = "rgba(34,197,94,0.9)"; } roundRect(ctx, x, y, blockW, blockH, 12); @@ -138,4 +145,5 @@ export function drawChainCanvas(now) { } } ctx.globalAlpha = 1; + ctx.restore(); } diff --git a/web/src/render/validators.js b/web/src/render/validators.js index b087bec..2ab7b5b 100644 --- a/web/src/render/validators.js +++ b/web/src/render/validators.js @@ -1,7 +1,7 @@ import { COLORS, state } from "../state.js"; import { validatorCanvas, validatorCtx } from "../dom.js"; import { getValidatorCount } from "../sim/logic.js"; -import { drawValidatorAvatar } from "./primitives.js"; +import { drawValidatorAvatar, roundRect } from "./primitives.js"; export function drawValidatorCanvas(now) { const ctx = validatorCtx; @@ -31,12 +31,31 @@ export function drawValidatorCanvas(now) { ctx.lineWidth = 1.2; ctx.stroke(); + const blockLabel = "BLOCK"; + const slotLabel = String(state.currentSlot); + ctx.textAlign = "center"; + + ctx.font = "700 11px 'Space Mono'"; + const labelW = ctx.measureText(blockLabel).width; + ctx.font = "700 13px 'Space Mono'"; + const slotW = ctx.measureText(slotLabel).width; + const boxW = Math.max(labelW, slotW) + 18; + const boxH = 34; + const boxX = cx - boxW / 2; + const boxY = cy - boxH / 2; + + roundRect(ctx, boxX, boxY, boxW, boxH, 8); + ctx.fillStyle = "rgba(6,10,18,0.85)"; + ctx.fill(); + ctx.lineWidth = 1; + ctx.strokeStyle = "rgba(0,255,204,0.35)"; + ctx.stroke(); + ctx.font = "700 11px 'Space Mono'"; ctx.fillStyle = "#c9fff4"; - ctx.textAlign = "center"; - ctx.fillText("BLOCK", cx, cy - 2); + ctx.fillText(blockLabel, cx, cy - 4); ctx.font = "700 13px 'Space Mono'"; - ctx.fillText(String(state.currentSlot), cx, cy + 12); + ctx.fillText(slotLabel, cx, cy + 11); const count = getValidatorCount(); const nodePos = []; diff --git a/web/src/state.js b/web/src/state.js index 80afa65..f43ef36 100644 --- a/web/src/state.js +++ b/web/src/state.js @@ -1,4 +1,4 @@ -export const DEMO_SLOT_MS = 8000; +export const DEMO_SLOT_MS = 4000; export const LIVE_SLOT_MS = 4000; export const DEFAULT_VALIDATOR_COUNT = 24; diff --git a/web/styles/main.css b/web/styles/main.css index ab32f94..088bb63 100644 --- a/web/styles/main.css +++ b/web/styles/main.css @@ -45,8 +45,8 @@ } .app { - width: min(1680px, calc(100vw - 24px)); - margin: 12px auto; + width: 100%; + margin: 0; border: 1px solid var(--border); background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.005)); box-shadow: 0 24px 80px rgba(0, 0, 0, 0.55); @@ -271,6 +271,7 @@ background: rgba(0,255,204,0.12); } + .debug { margin-top: 8px; border-top: 1px solid var(--border);