-
+
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/BTCPanel.module.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/BTCPanel.module.scss
new file mode 100644
index 00000000..c6bb247f
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/BTCPanel.module.scss
@@ -0,0 +1,59 @@
+// ©AngelaMos | 2026
+// BTCPanel.module.scss
+
+.hero {
+ display: flex;
+ align-items: baseline;
+ gap: 6px;
+ padding: 8px;
+}
+
+.price {
+ font-family: var(--font-mono);
+ font-size: var(--type-num-xl);
+ font-variant-numeric: tabular-nums;
+ color: var(--fg-1);
+ line-height: 1;
+}
+
+.unit {
+ font-size: var(--type-label);
+ letter-spacing: var(--letter-spacing-label);
+ color: var(--fg-3);
+ text-transform: uppercase;
+}
+
+.changes {
+ padding: 0 8px;
+}
+
+.change {
+ display: grid;
+ grid-template-columns: 36px auto;
+ gap: 8px;
+ align-items: center;
+ font-family: var(--font-mono);
+ font-size: var(--type-body);
+ padding: var(--row-py) 0;
+}
+
+.changeLabel {
+ font-family: var(--font-sans);
+ font-size: var(--type-label);
+ letter-spacing: var(--letter-spacing-label);
+ text-transform: uppercase;
+ color: var(--fg-3);
+}
+
+.changeValue {
+ color: var(--fg-1);
+ font-variant-numeric: tabular-nums;
+}
+
+.spark {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 8px;
+ color: var(--fg-2);
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/BTCPanel.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/BTCPanel.tsx
new file mode 100644
index 00000000..f8594086
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/BTCPanel.tsx
@@ -0,0 +1,122 @@
+// ©AngelaMos | 2026
+// BTCPanel.tsx
+
+import { useEffect } from 'react'
+import { useSnapshot } from '@/api/snapshot'
+import { usePrices } from '@/stores/prices'
+import styles from './BTCPanel.module.scss'
+import { Panel } from './Panel'
+import { Sparkline } from './shared/Sparkline'
+
+const SYMBOL = 'BTC-USD'
+const SPARKLINE_WIDTH = 280
+const SPARKLINE_HEIGHT = 22
+const PERCENT_DECIMALS = 2
+const PRICE_DECIMALS = 2
+const STALE_AFTER_MS = 60_000
+
+interface CoinbaseSnapshotTick {
+ symbol: string
+ ts: string
+ price: string
+ volume_24h?: string
+}
+
+export function BTCPanel(): React.ReactElement {
+ const { data } = useSnapshot()
+ const latest = usePrices((s) => s.latest[SYMBOL])
+ const history = usePrices((s) => s.history[SYMBOL])
+ const pushTick = usePrices((s) => s.pushTick)
+
+ const seed = data?.coinbase_price as CoinbaseSnapshotTick | undefined
+ useEffect(() => {
+ if (!seed || seed.symbol !== SYMBOL) return
+ pushTick({
+ symbol: seed.symbol,
+ ts: new Date(seed.ts).getTime(),
+ price: seed.price,
+ volume24h: seed.volume_24h,
+ })
+ }, [seed, pushTick])
+
+ const priceNum = latest ? Number(latest.price) : null
+ const closes = (history ?? []).map((b) => Number(b.close))
+ const pct1h = computeChangePct(closes)
+
+ const lastTickAt = latest?.ts
+ const isStale =
+ latest === undefined ? undefined : Date.now() - latest.ts > STALE_AFTER_MS
+
+ return (
+
+
+ {fmtPrice(priceNum)}
+ USD
+
+
+
+
+
+
+
+
+ )
+}
+
+BTCPanel.displayName = 'BTCPanel'
+
+function ChangeRow({
+ label,
+ pct,
+}: {
+ label: string
+ pct: number | null
+}): React.ReactElement {
+ return (
+
+ {label}
+ {fmtPct(pct)}
+
+ )
+}
+
+function computeChangePct(closes: number[]): number | null {
+ if (closes.length < 2) return null
+ const first = closes[0]
+ const last = closes[closes.length - 1]
+ if (
+ first === undefined ||
+ last === undefined ||
+ !Number.isFinite(first) ||
+ !Number.isFinite(last) ||
+ first === 0
+ ) {
+ return null
+ }
+ return ((last - first) / first) * 100
+}
+
+function fmtPct(pct: number | null): string {
+ if (pct === null) return '—'
+ const glyph = pct >= 0 ? '△' : '▽'
+ return `${glyph} ${Math.abs(pct).toFixed(PERCENT_DECIMALS)} %`
+}
+
+function fmtPrice(n: number | null): string {
+ if (n === null || !Number.isFinite(n)) return '—'
+ return n.toLocaleString(undefined, {
+ minimumFractionDigits: PRICE_DECIMALS,
+ maximumFractionDigits: PRICE_DECIMALS,
+ })
+}