[object Object]

← back to Abrams

v0.9: Abrams Terminal — Bloomberg-style markets + 8 proprietary indexes

ae3871bcf87da052f7c410fb3da41fce4257f752 · 2026-05-14 09:46:10 -0700 · Steve Abrams

market-data.js (live yahoo spark endpoint):
- 11 tickers: SPY/QQQ/DIA/IWM + ^VIX + ^TNX/^TYX/^IRX + GC=F/CL=F + BTC-USD
- spark sparklines (last ~30 closes) + price + previousClose + dayHigh/Low + 52w high/low
- 4s timeout per ticker, courtesy 120ms delay, fallback markers

abrams-indexes.js (8 proprietary indexes from real local data):
- ABI  Abrams Build Index   — commits/day vs prev 7d, indexed 100 (currently 675 = accelerating)
- OHI  Operator Heat        — pm2 uptime × commit velocity (100/100 hot)
- LNCI Late-Night Code      — % of 7d commits at 22:00-02:59
- API  Abrams Pizza Index   — direct count of late-night commits (Pentagon Pizza Index variant)
- WHSI Waffle House Stack   — green/yellow/orange/red on pm2 health (149/149 GREEN)
- LVI  Lead Velocity        — leads/7d
- TMCI TM Confidence        — 8-step weighted trademark checklist (85/100 ready to file)
- ESI  Empire Sprawl        — total LOC across portfolio (14,630)

/terminal page:
- Two-pane layout: markets left, abrams indexes right
- Sort by category/name/%change/price; group by category
- Inline SVG sparklines (green if up, red if down)
- Per-index methodology blurb, interpretation badge (HOT/WARM/COLD)
- Live banner with clock + data-stamp
- Auto-refresh every 60s
- Waffle House gets a colored left border by status (green/yellow/orange/red)

home panel:
- Terminal preview strip below counters: top 6 tickers + 3 abrams indexes inline
- Clickable → /terminal
- corner-nav drawer: Terminal as item 2 (right after Panel)

tick.sh: hooks phases 3.6 market refresh + 3.7 abrams-indexes refresh

Files touched

Diff

commit ae3871bcf87da052f7c410fb3da41fce4257f752
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu May 14 09:46:10 2026 -0700

    v0.9: Abrams Terminal — Bloomberg-style markets + 8 proprietary indexes
    
    market-data.js (live yahoo spark endpoint):
    - 11 tickers: SPY/QQQ/DIA/IWM + ^VIX + ^TNX/^TYX/^IRX + GC=F/CL=F + BTC-USD
    - spark sparklines (last ~30 closes) + price + previousClose + dayHigh/Low + 52w high/low
    - 4s timeout per ticker, courtesy 120ms delay, fallback markers
    
    abrams-indexes.js (8 proprietary indexes from real local data):
    - ABI  Abrams Build Index   — commits/day vs prev 7d, indexed 100 (currently 675 = accelerating)
    - OHI  Operator Heat        — pm2 uptime × commit velocity (100/100 hot)
    - LNCI Late-Night Code      — % of 7d commits at 22:00-02:59
    - API  Abrams Pizza Index   — direct count of late-night commits (Pentagon Pizza Index variant)
    - WHSI Waffle House Stack   — green/yellow/orange/red on pm2 health (149/149 GREEN)
    - LVI  Lead Velocity        — leads/7d
    - TMCI TM Confidence        — 8-step weighted trademark checklist (85/100 ready to file)
    - ESI  Empire Sprawl        — total LOC across portfolio (14,630)
    
    /terminal page:
    - Two-pane layout: markets left, abrams indexes right
    - Sort by category/name/%change/price; group by category
    - Inline SVG sparklines (green if up, red if down)
    - Per-index methodology blurb, interpretation badge (HOT/WARM/COLD)
    - Live banner with clock + data-stamp
    - Auto-refresh every 60s
    - Waffle House gets a colored left border by status (green/yellow/orange/red)
    
    home panel:
    - Terminal preview strip below counters: top 6 tickers + 3 abrams indexes inline
    - Clickable → /terminal
    - corner-nav drawer: Terminal as item 2 (right after Panel)
    
    tick.sh: hooks phases 3.6 market refresh + 3.7 abrams-indexes refresh
---
 data/abrams-indexes.json  | 142 ++++++++++++++
 data/build-log.jsonl      |  11 ++
 data/counters.json        |   6 +-
 data/mag-20-results.json  | 156 ++++-----------
 data/market.json          | 474 ++++++++++++++++++++++++++++++++++++++++++++++
 public/index.html         |  27 ++-
 public/js/corner-nav.js   |   1 +
 public/terminal.html      | 218 +++++++++++++++++++++
 scripts/abrams-indexes.js | 338 +++++++++++++++++++++++++++++++++
 scripts/market-data.js    |  99 ++++++++++
 scripts/tick.sh           |  10 +
 server.js                 |   3 +
 12 files changed, 1357 insertions(+), 128 deletions(-)

diff --git a/data/abrams-indexes.json b/data/abrams-indexes.json
new file mode 100644
index 00000000..e07be9db
--- /dev/null
+++ b/data/abrams-indexes.json
@@ -0,0 +1,142 @@
+{
+  "generated_at": "2026-05-14T16:43:53.834Z",
+  "indexes": [
+    {
+      "id": "ABI",
+      "name": "Abrams Build Index",
+      "icon": "◐",
+      "value": 675,
+      "unit": "idx",
+      "delta": 6.57,
+      "delta_label": "commits/day vs prev 7d",
+      "sparkline": [
+        0,
+        3,
+        19,
+        5,
+        1,
+        7,
+        19
+      ],
+      "method": "Daily commit count across all Abrams projects, indexed 100 = previous 7-day average. >100 = accelerating. <100 = cooling.",
+      "interpretation": "BUILD ACCELERATING"
+    },
+    {
+      "id": "OHI",
+      "name": "Operator Heat Index",
+      "icon": "✸",
+      "value": 100,
+      "unit": "/100",
+      "delta": 11,
+      "delta_label": "commits today vs 7d avg",
+      "sparkline": [
+        0,
+        3,
+        19,
+        5,
+        1,
+        7,
+        19
+      ],
+      "method": "Composite: 50pt for pm2 uptime ratio + 50pt cap for today commits × 5. Reads founder activity intensity.",
+      "interpretation": "HOT"
+    },
+    {
+      "id": "LNCI",
+      "name": "Late-Night Code Index",
+      "icon": "☾",
+      "value": 19,
+      "unit": "%",
+      "delta": 11,
+      "delta_label": "late-night commits in 7d",
+      "sparkline": [
+        0,
+        1,
+        10,
+        0,
+        0,
+        0,
+        0
+      ],
+      "method": "% of last-7-day commits landing between 22:00-02:59 local time. Productivity-vs-burnout signal — sustained >40% reads \"founder is running hot.\"",
+      "interpretation": "DAYTIME OPERATOR"
+    },
+    {
+      "id": "API",
+      "name": "Abrams Pizza Index",
+      "icon": "🍕",
+      "value": 11,
+      "unit": "late-night commits / 7d",
+      "delta": 0,
+      "delta_label": "last 24h vs prior day",
+      "sparkline": [
+        0,
+        1,
+        10,
+        0,
+        0,
+        0,
+        0
+      ],
+      "method": "Direct count of commits landing 22:00-02:59 local in the last 7 days. The original Pentagon Pizza Index tracks pizza orders to DOD; the Abrams variant tracks late-night commits. Spikes precede shipped features.",
+      "interpretation": "NIGHT BUILD ACTIVE"
+    },
+    {
+      "id": "WHSI",
+      "name": "Waffle House Stack Index",
+      "icon": "🧇",
+      "value": "GREEN",
+      "color": "#4ade80",
+      "unit": "149/149",
+      "delta": 0,
+      "delta_label": "all systems operational",
+      "sparkline": null,
+      "method": "FEMA's Waffle House Index measures disaster severity by whether Waffle House is serving. The Abrams variant: GREEN if every pm2 process online, YELLOW if 80-99%, ORANGE if 50-79%, RED if <50%. Real-time stack health.",
+      "interpretation": "FULL MENU — all services online"
+    },
+    {
+      "id": "LVI",
+      "name": "Lead Velocity Index",
+      "icon": "⟶",
+      "value": 1,
+      "unit": "leads / 7d",
+      "delta": 1,
+      "delta_label": "vs prior 7d",
+      "sparkline": [
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        1
+      ],
+      "method": "Leads captured in the last 7 days via /api/lead. Compare to prior 7d for momentum. Baseline build phase = 0-1; growth phase = 5+.",
+      "interpretation": "EARLY SIGNAL"
+    },
+    {
+      "id": "TMCI",
+      "name": "TM Confidence Index",
+      "icon": "®",
+      "value": 85,
+      "unit": "/100",
+      "delta": 0,
+      "delta_label": "7/8 steps complete",
+      "sparkline": null,
+      "method": "Weighted checklist: mark+owner identified (20pt), 5+ classes selected (20), filing routes researched (15), domain status (10), common-law noted (10), next-action plan (10), USPTO filing submitted (15).",
+      "interpretation": "FILED"
+    },
+    {
+      "id": "ESI",
+      "name": "Empire Sprawl Index",
+      "icon": "✦",
+      "value": 14691,
+      "unit": "LOC",
+      "delta": null,
+      "delta_label": "across 8 projects",
+      "sparkline": null,
+      "method": "Total lines of code across every Abrams project (.js / .ts / .tsx / .html / .css / .py / .sh / .md, excluding node_modules + .git). One scalar for the size of the empire.",
+      "interpretation": "EARLY"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/data/build-log.jsonl b/data/build-log.jsonl
index e09fddc8..553a9a56 100644
--- a/data/build-log.jsonl
+++ b/data/build-log.jsonl
@@ -64,3 +64,14 @@
 {"ts":"2026-05-14T16:32:06Z","type":"inventory-ok","phase":"inventory"}
 {"ts":"2026-05-14T16:32:06.985Z","type":"swot","slug":"abrams-panel","name":"Abrams Panel"}
 {"ts":"2026-05-14T16:32:06Z","type":"swot-ok","phase":"swot"}
+{"ts":"2026-05-14T16:32:06Z","type":"git-commit","phase":"git"}
+{"ts":"2026-05-14T16:32:07.212Z","type":"counters","count":7}
+{"ts":"2026-05-14T16:32:06Z","type":"counters-ok","phase":"counters"}
+{"ts":"2026-05-14T16:32:08.036Z","type":"mag-20-audit","projects":12,"total_pass":68,"total_fail":30}
+{"ts":"2026-05-14T16:32:06Z","type":"mag-20-ok","phase":"audit"}
+{"ts":"2026-05-14T16:32:08Z","type":"drain-queue","drained":0,"failed":3,"pending":3}
+{"ts":"2026-05-14T16:32:06Z","type":"tick-done","tick":"1778776328"}
+{"ts":"2026-05-14T16:43:21.942Z","type":"abrams-indexes","count":8}
+{"ts":"2026-05-14T16:43:24.245Z","type":"market-data","ok":11,"err":0,"total":11}
+{"ts":"2026-05-14T16:43:53.834Z","type":"abrams-indexes","count":8}
+{"ts":"2026-05-14T16:45:17.779Z","type":"server-up","port":9898,"bind":"0.0.0.0"}
diff --git a/data/counters.json b/data/counters.json
index 4e163f87..1970b2c1 100644
--- a/data/counters.json
+++ b/data/counters.json
@@ -1,10 +1,10 @@
 {
-  "generated_at": "2026-05-14T16:25:17.599Z",
+  "generated_at": "2026-05-14T16:32:07.211Z",
   "counters": [
     {
       "key": "projects",
       "label": "Projects tracked",
-      "value": 13,
+      "value": 12,
       "url": "/#projects"
     },
     {
@@ -40,7 +40,7 @@
     {
       "key": "commits",
       "label": "Commits across portfolio",
-      "value": 52,
+      "value": 60,
       "url": null
     }
   ]
diff --git a/data/mag-20-results.json b/data/mag-20-results.json
index b3622459..53931424 100644
--- a/data/mag-20-results.json
+++ b/data/mag-20-results.json
@@ -1,96 +1,6 @@
 {
-  "generated_at": "2026-05-14T16:20:53.306Z",
+  "generated_at": "2026-05-14T16:32:07.243Z",
   "projects": [
-    {
-      "slug": "wallco-ai",
-      "name": "wallco.ai",
-      "status": "live",
-      "checks": {
-        "1": {
-          "status": "n/a",
-          "evidence": "no html files"
-        },
-        "2": {
-          "status": "n/a",
-          "evidence": "no html files"
-        },
-        "3": {
-          "status": "n/a",
-          "evidence": "no index.html"
-        },
-        "4": {
-          "status": "n/a",
-          "evidence": "rotating hero rule is for DW microsites, not panels"
-        },
-        "5": {
-          "status": "n/a",
-          "evidence": "no grid pages"
-        },
-        "6": {
-          "status": "n/a",
-          "evidence": "no html files"
-        },
-        "7": {
-          "status": "n/a",
-          "evidence": "no contact modal trigger"
-        },
-        "8": {
-          "status": "n/a",
-          "evidence": "customer-chat rule (lower-LEFT woman avatar) is DW-customer-facing only"
-        },
-        "9": {
-          "status": "n/a",
-          "evidence": "chip rule applies to DW catalog grids"
-        },
-        "10": {
-          "status": "n/a",
-          "evidence": "no html files"
-        },
-        "11": {
-          "status": "pass",
-          "evidence": ".git/ present"
-        },
-        "12": {
-          "status": "pass",
-          "evidence": "327 commits over 2 days (163.50/day)"
-        },
-        "13": {
-          "status": "pass",
-          "evidence": "all 5 essentials present"
-        },
-        "14": {
-          "status": "n/a",
-          "evidence": "no html files"
-        },
-        "15": {
-          "status": "n/a",
-          "evidence": "no mailer in this project"
-        },
-        "16": {
-          "status": "n/a",
-          "evidence": "no mailer in this project"
-        },
-        "17": {
-          "status": "fail",
-          "evidence": "127.0.0.1 default"
-        },
-        "18": {
-          "status": "pass",
-          "evidence": "2 limiters / 41 POSTs"
-        },
-        "19": {
-          "status": "n/a",
-          "evidence": "no hourly mailer in this project"
-        },
-        "20": {
-          "status": "pass",
-          "evidence": "259 what-landed videos exist; per-project mapping TBD"
-        }
-      },
-      "score": 5,
-      "fail": 1,
-      "na": 14
-    },
     {
       "slug": "abrams-panel",
       "name": "Abrams Panel",
@@ -98,7 +8,7 @@
       "checks": {
         "1": {
           "status": "fail",
-          "evidence": "1/6 missing chrome"
+          "evidence": "1/10 missing chrome"
         },
         "2": {
           "status": "pass",
@@ -134,7 +44,7 @@
         },
         "10": {
           "status": "fail",
-          "evidence": "1/6 missing viewport meta"
+          "evidence": "1/10 missing viewport meta"
         },
         "11": {
           "status": "pass",
@@ -142,7 +52,7 @@
         },
         "12": {
           "status": "pass",
-          "evidence": "9 commits over 1 days (9.00/day)"
+          "evidence": "14 commits over 1 days (14.00/day)"
         },
         "13": {
           "status": "pass",
@@ -166,7 +76,7 @@
         },
         "18": {
           "status": "pass",
-          "evidence": "7 limiters / 3 POSTs"
+          "evidence": "10 limiters / 4 POSTs"
         },
         "19": {
           "status": "pass",
@@ -231,8 +141,8 @@
           "evidence": ".git/ present"
         },
         "12": {
-          "status": "fail",
-          "evidence": "2 commits over 1 days (2.00/day)"
+          "status": "pass",
+          "evidence": "4 commits over 1 days (4.00/day)"
         },
         "13": {
           "status": "pass",
@@ -267,8 +177,8 @@
           "evidence": "259 what-landed videos exist; per-project mapping TBD"
         }
       },
-      "score": 6,
-      "fail": 6,
+      "score": 7,
+      "fail": 5,
       "na": 8
     },
     {
@@ -278,7 +188,7 @@
       "checks": {
         "1": {
           "status": "fail",
-          "evidence": "1/6 missing chrome"
+          "evidence": "1/10 missing chrome"
         },
         "2": {
           "status": "pass",
@@ -314,7 +224,7 @@
         },
         "10": {
           "status": "fail",
-          "evidence": "1/6 missing viewport meta"
+          "evidence": "1/10 missing viewport meta"
         },
         "11": {
           "status": "pass",
@@ -322,7 +232,7 @@
         },
         "12": {
           "status": "pass",
-          "evidence": "9 commits over 1 days (9.00/day)"
+          "evidence": "14 commits over 1 days (14.00/day)"
         },
         "13": {
           "status": "pass",
@@ -346,7 +256,7 @@
         },
         "18": {
           "status": "pass",
-          "evidence": "7 limiters / 3 POSTs"
+          "evidence": "10 limiters / 4 POSTs"
         },
         "19": {
           "status": "pass",
@@ -1176,100 +1086,100 @@
     "1": {
       "pass": 0,
       "fail": 6,
-      "na": 7
+      "na": 6
     },
     "2": {
       "pass": 2,
       "fail": 4,
-      "na": 7
+      "na": 6
     },
     "3": {
       "pass": 2,
       "fail": 4,
-      "na": 7
+      "na": 6
     },
     "4": {
       "pass": 0,
       "fail": 0,
-      "na": 13
+      "na": 12
     },
     "5": {
       "pass": 3,
       "fail": 2,
-      "na": 8
+      "na": 7
     },
     "6": {
       "pass": 2,
       "fail": 4,
-      "na": 7
+      "na": 6
     },
     "7": {
       "pass": 0,
       "fail": 0,
-      "na": 13
+      "na": 12
     },
     "8": {
       "pass": 0,
       "fail": 0,
-      "na": 13
+      "na": 12
     },
     "9": {
       "pass": 0,
       "fail": 0,
-      "na": 13
+      "na": 12
     },
     "10": {
       "pass": 3,
       "fail": 3,
-      "na": 7
+      "na": 6
     },
     "11": {
-      "pass": 11,
+      "pass": 10,
       "fail": 0,
       "na": 2
     },
     "12": {
       "pass": 9,
-      "fail": 2,
+      "fail": 1,
       "na": 2
     },
     "13": {
-      "pass": 11,
+      "pass": 10,
       "fail": 0,
       "na": 2
     },
     "14": {
       "pass": 4,
       "fail": 2,
-      "na": 7
+      "na": 6
     },
     "15": {
       "pass": 2,
       "fail": 0,
-      "na": 11
+      "na": 10
     },
     "16": {
       "pass": 2,
       "fail": 0,
-      "na": 11
+      "na": 10
     },
     "17": {
       "pass": 5,
-      "fail": 3,
+      "fail": 2,
       "na": 5
     },
     "18": {
-      "pass": 3,
+      "pass": 2,
       "fail": 2,
       "na": 8
     },
     "19": {
       "pass": 2,
       "fail": 0,
-      "na": 11
+      "na": 10
     },
     "20": {
-      "pass": 11,
+      "pass": 10,
       "fail": 0,
       "na": 2
     }
diff --git a/data/market.json b/data/market.json
new file mode 100644
index 00000000..b074ce4d
--- /dev/null
+++ b/data/market.json
@@ -0,0 +1,474 @@
+{
+  "generated_at": "2026-05-14T16:43:21.972Z",
+  "tickers": [
+    {
+      "sym": "SPY",
+      "name": "S&P 500 ETF",
+      "cat": "Stocks",
+      "symbol": "SPY",
+      "price": 748.145,
+      "previousClose": 694.46,
+      "dayHigh": 749.53,
+      "dayLow": 743.56,
+      "fiftyTwoWeekHigh": 749.53,
+      "fiftyTwoWeekLow": 575.6,
+      "sparkline": [
+        694.46,
+        699.94,
+        701.66,
+        710.14,
+        708.72,
+        704.08,
+        711.21,
+        708.45,
+        713.94,
+        715.17,
+        711.69,
+        711.58,
+        718.66,
+        720.65,
+        718.01,
+        723.77,
+        733.83,
+        731.58,
+        737.62,
+        739.3,
+        738.18,
+        742.31,
+        748.145
+      ],
+      "currency": "USD",
+      "exchangeName": "PCX",
+      "change": 53.685,
+      "changePct": 7.7305,
+      "updated_at": "2026-05-14T16:43:22.139Z"
+    },
+    {
+      "sym": "QQQ",
+      "name": "Nasdaq-100 ETF",
+      "cat": "Stocks",
+      "symbol": "QQQ",
+      "price": 720.88,
+      "previousClose": 628.6,
+      "dayHigh": 722.03,
+      "dayLow": 714.22,
+      "fiftyTwoWeekHigh": 722.03,
+      "fiftyTwoWeekLow": 505.58,
+      "sparkline": [
+        628.6,
+        637.4,
+        640.47,
+        648.85,
+        646.79,
+        644.33,
+        655.11,
+        651.42,
+        663.88,
+        664.23,
+        657.55,
+        661.57,
+        667.74,
+        674.15,
+        672.88,
+        681.61,
+        695.77,
+        694.94,
+        711.23,
+        713.29,
+        707.24,
+        714.71,
+        720.88
+      ],
+      "currency": "USD",
+      "exchangeName": "NGM",
+      "change": 92.28,
+      "changePct": 14.6802,
+      "updated_at": "2026-05-14T16:43:22.381Z"
+    },
+    {
+      "sym": "DIA",
+      "name": "Dow Industrials ETF",
+      "cat": "Stocks",
+      "symbol": "DIA",
+      "price": 500.82,
+      "previousClose": 485.49,
+      "dayHigh": 502.16,
+      "dayLow": 498.85,
+      "fiftyTwoWeekHigh": 505.3,
+      "fiftyTwoWeekLow": 413.83,
+      "sparkline": [
+        485.49,
+        484.72,
+        485.63,
+        494.22,
+        494.33,
+        491.36,
+        494.76,
+        493,
+        492.21,
+        491.83,
+        491.42,
+        488.67,
+        496.65,
+        495.02,
+        489.56,
+        492.96,
+        499.05,
+        495.91,
+        496.13,
+        497.11,
+        497.89,
+        497.14,
+        500.82
+      ],
+      "currency": "USD",
+      "exchangeName": "PCX",
+      "change": 15.33,
+      "changePct": 3.1576,
+      "updated_at": "2026-05-14T16:43:22.653Z"
+    },
+    {
+      "sym": "IWM",
+      "name": "Russell 2000 ETF",
+      "cat": "Stocks",
+      "symbol": "IWM",
+      "price": 284.83,
+      "previousClose": 268.72,
+      "dayHigh": 285.655,
+      "dayLow": 281.96,
+      "fiftyTwoWeekHigh": 287.58,
+      "fiftyTwoWeekLow": 199.65,
+      "sparkline": [
+        268.72,
+        269.39,
+        269.95,
+        275.78,
+        277.35,
+        274.51,
+        276.48,
+        275.52,
+        276.65,
+        277.14,
+        273.91,
+        272.08,
+        277.97,
+        279.28,
+        277.88,
+        282.56,
+        286.8,
+        282.26,
+        284.17,
+        285.33,
+        282.57,
+        282.67,
+        284.83
+      ],
+      "currency": "USD",
+      "exchangeName": "PCX",
+      "change": 16.11,
+      "changePct": 5.9951,
+      "updated_at": "2026-05-14T16:43:22.825Z"
+    },
+    {
+      "sym": "^VIX",
+      "name": "CBOE Volatility",
+      "cat": "Stocks",
+      "symbol": "^VIX",
+      "price": 17.73,
+      "previousClose": 18.36,
+      "dayHigh": 18.08,
+      "dayLow": 17.72,
+      "fiftyTwoWeekHigh": 35.3,
+      "fiftyTwoWeekLow": 13.38,
+      "sparkline": [
+        18.36,
+        18.17,
+        17.94,
+        17.48,
+        18.87,
+        19.5,
+        18.92,
+        19.31,
+        18.71,
+        18.02,
+        17.83,
+        18.81,
+        16.89,
+        16.99,
+        18.29,
+        17.38,
+        17.39,
+        17.08,
+        17.19,
+        18.38,
+        17.99,
+        17.87,
+        17.73
+      ],
+      "currency": "USD",
+      "exchangeName": "CXI",
+      "change": -0.63,
+      "changePct": -3.4314,
+      "updated_at": "2026-05-14T16:43:23.007Z"
+    },
+    {
+      "sym": "^TNX",
+      "name": "10-Year Treasury",
+      "cat": "Bonds",
+      "symbol": "^TNX",
+      "price": 4.449,
+      "previousClose": 4.256,
+      "dayHigh": 4.461,
+      "dayLow": 4.436,
+      "fiftyTwoWeekHigh": 4.997,
+      "fiftyTwoWeekLow": 3.345,
+      "sparkline": [
+        4.256,
+        4.282,
+        4.309,
+        4.246,
+        4.25,
+        4.292,
+        4.294,
+        4.323,
+        4.31,
+        4.336,
+        4.354,
+        4.418,
+        4.39,
+        4.378,
+        4.446,
+        4.416,
+        4.356,
+        4.392,
+        4.364,
+        4.41,
+        4.463,
+        4.481,
+        4.449
+      ],
+      "currency": "USD",
+      "exchangeName": "CGI",
+      "change": 0.193,
+      "changePct": 4.5348,
+      "updated_at": "2026-05-14T16:43:23.185Z"
+    },
+    {
+      "sym": "^TYX",
+      "name": "30-Year Treasury",
+      "cat": "Bonds",
+      "symbol": "^TYX",
+      "price": 5.002,
+      "previousClose": 4.868,
+      "dayHigh": 5.014,
+      "dayLow": 4.995,
+      "fiftyTwoWeekHigh": 5.152,
+      "fiftyTwoWeekLow": 3.717,
+      "sparkline": [
+        4.868,
+        4.891,
+        4.929,
+        4.885,
+        4.881,
+        4.898,
+        4.902,
+        4.918,
+        4.916,
+        4.942,
+        4.944,
+        4.987,
+        4.987,
+        4.966,
+        5.025,
+        4.984,
+        4.943,
+        4.969,
+        4.947,
+        4.986,
+        5.031,
+        5.047,
+        5.002
+      ],
+      "currency": "USD",
+      "exchangeName": "CGI",
+      "change": 0.134,
+      "changePct": 2.7527,
+      "updated_at": "2026-05-14T16:43:23.365Z"
+    },
+    {
+      "sym": "^IRX",
+      "name": "13-Week T-Bill",
+      "cat": "Bonds",
+      "symbol": "^IRX",
+      "price": 3.588,
+      "previousClose": 3.612,
+      "dayHigh": 3.593,
+      "dayLow": 3.588,
+      "fiftyTwoWeekHigh": 5.348,
+      "fiftyTwoWeekLow": 3.588,
+      "sparkline": [
+        3.612,
+        3.612,
+        3.61,
+        3.6,
+        3.598,
+        3.598,
+        3.595,
+        3.595,
+        3.593,
+        3.59,
+        3.59,
+        3.585,
+        3.575,
+        3.59,
+        3.6,
+        3.6,
+        3.598,
+        3.595,
+        3.6,
+        3.603,
+        3.6,
+        3.588
+      ],
+      "currency": "USD",
+      "exchangeName": "CGI",
+      "change": -0.024,
+      "changePct": -0.6645,
+      "updated_at": "2026-05-14T16:43:23.535Z"
+    },
+    {
+      "sym": "GC=F",
+      "name": "Gold Futures",
+      "cat": "Commodities",
+      "symbol": "GC=F",
+      "price": 4681.3,
+      "previousClose": 4825,
+      "dayHigh": 4725.8,
+      "dayLow": 4671.3,
+      "fiftyTwoWeekHigh": 5586.2,
+      "fiftyTwoWeekLow": 3125,
+      "sparkline": [
+        4825,
+        4800,
+        4785.4,
+        4857.6,
+        4806.6,
+        4698.4,
+        4732.5,
+        4705.1,
+        4722.3,
+        4675.4,
+        4591.5,
+        4545.2,
+        4614.7,
+        4629.9,
+        4519.5,
+        4555.8,
+        4681.9,
+        4699.8,
+        4720.4,
+        4718.7,
+        4677.6,
+        4697.7,
+        4681.3
+      ],
+      "currency": "USD",
+      "exchangeName": "CMX",
+      "change": -143.7,
+      "changePct": -2.9782,
+      "updated_at": "2026-05-14T16:43:23.770Z"
+    },
+    {
+      "sym": "CL=F",
+      "name": "WTI Crude Oil",
+      "cat": "Commodities",
+      "symbol": "CL=F",
+      "price": 101.04,
+      "previousClose": 91.28,
+      "dayHigh": 102.35,
+      "dayLow": 99.39,
+      "fiftyTwoWeekHigh": 119.48,
+      "fiftyTwoWeekLow": 54.98,
+      "sparkline": [
+        91.28,
+        91.29,
+        94.69,
+        83.85,
+        89.61,
+        92.13,
+        92.96,
+        95.85,
+        94.4,
+        96.37,
+        99.93,
+        106.88,
+        105.07,
+        101.94,
+        106.42,
+        102.27,
+        95.08,
+        94.81,
+        95.42,
+        98.07,
+        102.18,
+        101.02,
+        101.04
+      ],
+      "currency": "USD",
+      "exchangeName": "NYM",
+      "change": 9.76,
+      "changePct": 10.6924,
+      "updated_at": "2026-05-14T16:43:23.942Z"
+    },
+    {
+      "sym": "BTC-USD",
+      "name": "Bitcoin",
+      "cat": "Crypto",
+      "symbol": "BTC-USD",
+      "price": 81627.2,
+      "previousClose": 74181.61,
+      "dayHigh": 81542.305,
+      "dayLow": 79001.51,
+      "fiftyTwoWeekHigh": 126198.07,
+      "fiftyTwoWeekLow": 60074.203,
+      "sparkline": [
+        74181.61,
+        74805.08,
+        75152.13,
+        77126.875,
+        75726.21,
+        73856.35,
+        75872.52,
+        76352.77,
+        78203.1,
+        78268.95,
+        77455.31,
+        77612.016,
+        78657.54,
+        77366.625,
+        76350.67,
+        75776.13,
+        76304.32,
+        78179,
+        78657.25,
+        78538.23,
+        79827.91,
+        80927.055,
+        81427.53,
+        80009.99,
+        80186.766,
+        80664.37,
+        82138.93,
+        81728.3,
+        80477.49,
+        79277.12,
+        81627.2
+      ],
+      "currency": "USD",
+      "exchangeName": "CCC",
+      "change": 7445.59,
+      "changePct": 10.037,
+      "updated_at": "2026-05-14T16:43:24.123Z"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 7b0c51a6..beb1d4f0 100644
--- a/public/index.html
+++ b/public/index.html
@@ -62,6 +62,13 @@
 <!-- live counters strip (cross-project metrics from real services) -->
 <div id="counters-strip" style="background:#000;border-top:1px solid var(--hairline);border-bottom:1px solid var(--hairline);padding:14px 24px;display:flex;gap:32px;overflow-x:auto;flex-wrap:nowrap;align-items:center;"></div>
 
+<!-- terminal preview strip — live markets + top abrams index -->
+<a href="/terminal" style="text-decoration:none;display:block;background:#000;border-bottom:1px solid var(--hairline);padding:10px 24px;color:var(--fg);font-family:var(--mono);font-size:11px;overflow-x:auto;white-space:nowrap;">
+  <span style="color:var(--accent);letter-spacing:.25em;text-transform:uppercase;margin-right:18px;">▌ TERMINAL</span>
+  <span id="term-preview" style="color:var(--fg-2);">loading market feed…</span>
+  <span style="color:var(--fg-3);letter-spacing:.2em;float:right;">CLICK FOR FULL TERMINAL →</span>
+</a>
+
 <!-- live ticker -->
 <div class="ticker" id="ticker"><span class="dot"></span> <span id="ticker-text">connecting to build feed…</span></div>
 
@@ -131,6 +138,22 @@
 <script src="/static/js/gamify.js"></script>
 <script>
 // ── projects grid ──
+async function loadTermPreview() {
+  try {
+    const [m, a] = await Promise.all([
+      fetch('/api/market').then(r => r.json()),
+      fetch('/api/abrams-indexes').then(r => r.json()),
+    ]);
+    const top = (m.tickers || []).slice(0, 6).map(t => {
+      const sign = (t.changePct||0) >= 0 ? '+' : '';
+      const cls = (t.changePct||0) >= 0 ? 'color:#4ade80' : 'color:#ef4444';
+      return `<span style="margin-right:18px;"><strong style="color:#fff">${escapeHtml(t.sym)}</strong> ${(t.price||0).toFixed(2)} <span style="${cls}">${sign}${(t.changePct||0).toFixed(2)}%</span></span>`;
+    }).join('');
+    const ix = (a.indexes || []).slice(0, 3).map(x => `<span style="margin-right:18px;"><strong style="color:#c9a96e">${escapeHtml(x.id)}</strong> ${escapeHtml(String(x.value))}${escapeHtml(x.unit||'')}</span>`).join('');
+    document.getElementById('term-preview').innerHTML = top + ' &nbsp;·&nbsp; ' + ix;
+  } catch {}
+}
+
 async function loadCounters() {
   const r = await fetch('/api/counters').then(x=>x.json()).catch(()=>({counters:[]}));
   document.getElementById('counters-strip').innerHTML = (r.counters||[]).map(c => `
@@ -255,8 +278,8 @@ async function loadLeaderboard() {
   `;
 }
 
-loadProjects(); loadIdeas(); loadInitialFeed(); startStream(); loadLeaderboard(); loadCounters();
-setInterval(()=>{loadProjects();loadIdeas();loadLeaderboard();loadCounters();}, 30_000);
+loadProjects(); loadIdeas(); loadInitialFeed(); startStream(); loadLeaderboard(); loadCounters(); loadTermPreview();
+setInterval(()=>{loadProjects();loadIdeas();loadLeaderboard();loadCounters();loadTermPreview();}, 30_000);
 </script>
 </body>
 </html>
diff --git a/public/js/corner-nav.js b/public/js/corner-nav.js
index e48e2eef..f2d71a7a 100644
--- a/public/js/corner-nav.js
+++ b/public/js/corner-nav.js
@@ -3,6 +3,7 @@
   const cfg = window.CornerNavConfig || {};
   const links = cfg.links || [
     { label: 'Panel', href: '/' },
+    { label: 'Terminal', href: '/terminal' },
     { label: 'Thesis', href: '/#thesis' },
     { label: 'Projects', href: '/#projects' },
     { label: 'Ideas + SWOT', href: '/ideas' },
diff --git a/public/terminal.html b/public/terminal.html
new file mode 100644
index 00000000..281a9193
--- /dev/null
+++ b/public/terminal.html
@@ -0,0 +1,218 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1" />
+<title>Abrams Terminal</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;1,400&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
+<link rel="stylesheet" href="/static/css/panel.css" />
+<style>
+  body { background: #000; }
+  .term-shell { padding: 80px 18px 40px; max-width: 1640px; margin: 0 auto; }
+  .term-banner { background: #000; color: #c9a96e; font-family: var(--mono); font-size: 11px; letter-spacing: .25em; text-transform: uppercase; padding: 8px 14px; border: 1px solid #2a2620; display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; }
+  .term-banner .live { display: inline-flex; align-items: center; gap: 8px; }
+  .term-banner .live .dot { width: 7px; height: 7px; background: #4ade80; border-radius: 50%; animation: tpulse 1.4s infinite ease-in-out; }
+  @keyframes tpulse { 0%,100% { opacity: 1; } 50% { opacity: .3; } }
+
+  .term-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
+  @media (max-width: 1100px) { .term-grid { grid-template-columns: 1fr; } }
+
+  .term-pane { background: #0a0a0a; border: 1px solid #2a2620; }
+  .term-pane h2 { font-family: var(--mono); font-size: 11px; letter-spacing: .3em; text-transform: uppercase; color: #c9a96e; padding: 12px 18px; margin: 0; border-bottom: 1px solid #2a2620; display: flex; justify-content: space-between; align-items: center; }
+  .term-pane h2 .meta { color: #6a6457; font-weight: 400; letter-spacing: .15em; }
+
+  table.term { width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: 12px; color: #f1ece4; }
+  table.term thead th { background: #141414; color: #6a6457; font-weight: 500; padding: 8px 12px; text-align: right; font-size: 9px; letter-spacing: .25em; text-transform: uppercase; border-bottom: 1px solid #2a2620; }
+  table.term thead th:first-child, table.term thead th.left { text-align: left; }
+  table.term tbody td { padding: 8px 12px; text-align: right; border-bottom: 1px solid #1d1d1d; vertical-align: middle; }
+  table.term tbody td:first-child { text-align: left; font-weight: 600; color: #f1ece4; }
+  table.term tbody td.cat { font-size: 9px; color: #6a6457; letter-spacing: .15em; text-transform: uppercase; }
+  table.term tbody td.up { color: #4ade80; }
+  table.term tbody td.down { color: #ef4444; }
+  table.term tbody td.flat { color: #6a6457; }
+  table.term tbody tr:hover { background: rgba(201,169,110,.04); }
+
+  /* sparkline */
+  .spark { width: 100px; height: 28px; vertical-align: middle; }
+
+  /* Abrams indexes — denser, lozenge-card style */
+  .abrams-ix-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; padding: 14px; }
+  @media (max-width: 600px) { .abrams-ix-grid { grid-template-columns: 1fr; } }
+  .abrams-ix { background: #0d0d0d; border: 1px solid #2a2620; padding: 14px 16px; position: relative; }
+  .abrams-ix .head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px; }
+  .abrams-ix .id { font-family: var(--mono); font-size: 10px; color: #6a6457; letter-spacing: .25em; }
+  .abrams-ix .icon { font-size: 18px; line-height: 1; }
+  .abrams-ix .name { font-family: var(--serif); font-size: 16px; color: #f1ece4; margin: 2px 0 8px; font-weight: 500; }
+  .abrams-ix .value { font-family: var(--serif); font-size: 36px; color: #c9a96e; line-height: 1; letter-spacing: .02em; font-weight: 500; }
+  .abrams-ix .unit { font-family: var(--mono); font-size: 10px; color: #6a6457; margin-left: 6px; letter-spacing: .15em; text-transform: uppercase; }
+  .abrams-ix .interp { font-family: var(--mono); font-size: 9px; color: #4ade80; letter-spacing: .2em; text-transform: uppercase; margin: 8px 0 6px; }
+  .abrams-ix .interp.warn { color: #f59e0b; } .abrams-ix .interp.err { color: #ef4444; }
+  .abrams-ix .method { font-family: var(--sans); font-size: 11px; color: #8a8478; line-height: 1.5; margin: 8px 0 0; }
+  .abrams-ix .delta { font-family: var(--mono); font-size: 10px; color: #6a6457; letter-spacing: .15em; margin-top: 4px; }
+  .abrams-ix .delta.up { color: #4ade80; } .abrams-ix .delta.down { color: #ef4444; }
+  .abrams-ix .spark-wrap { position: absolute; bottom: 14px; right: 16px; }
+  .abrams-ix.whsi.green { border-left: 4px solid #4ade80; }
+  .abrams-ix.whsi.yellow { border-left: 4px solid #f59e0b; }
+  .abrams-ix.whsi.orange { border-left: 4px solid #fb923c; }
+  .abrams-ix.whsi.red { border-left: 4px solid #ef4444; }
+
+  /* controls */
+  .term-controls { display: flex; gap: 12px; padding: 12px 14px; align-items: center; background: #050505; border-bottom: 1px solid #2a2620; }
+  .term-controls .lbl { font-family: var(--mono); font-size: 9px; color: #6a6457; letter-spacing: .25em; text-transform: uppercase; }
+  .term-controls select, .term-controls input { background: #0a0a0a; border: 1px solid #2a2620; color: #f1ece4; padding: 5px 10px; font-family: var(--mono); font-size: 11px; }
+</style>
+</head>
+<body>
+
+<div class="chrome">
+  <a class="logo" href="/">ABRAMS<span class="dot">.</span></a>
+  <div class="nav-burger" role="button" aria-label="open menu">
+    <span class="bars"><span></span><span></span><span></span></span>
+    <span class="label">MENU</span>
+  </div>
+</div>
+
+<div class="term-shell">
+  <div class="term-banner">
+    <span class="live"><span class="dot"></span> ABRAMS TERMINAL · LIVE</span>
+    <span id="term-clock" style="color:#f1ece4;">--:--:--</span>
+    <span>MARKET <span id="market-stamp">--</span> · INDEX <span id="abrams-stamp">--</span></span>
+  </div>
+
+  <div class="term-grid">
+
+    <!-- LEFT: Bloomberg-style market panel -->
+    <div class="term-pane">
+      <h2>Markets · Stocks · Bonds · Commodities · Crypto <span class="meta" id="market-count">--</span></h2>
+      <div class="term-controls">
+        <span class="lbl">Sort</span>
+        <select id="sort-market">
+          <option value="cat">Category</option>
+          <option value="name">Name</option>
+          <option value="changePct">% Change</option>
+          <option value="price">Price</option>
+        </select>
+        <span class="lbl">Group</span>
+        <select id="group-market">
+          <option value="">none</option>
+          <option value="cat" selected>by category</option>
+        </select>
+        <span class="lbl">Refresh</span>
+        <button onclick="loadMarket()" style="background:#1d1d1d;border:1px solid #2a2620;color:#f1ece4;padding:5px 12px;font-family:var(--mono);font-size:10px;letter-spacing:.2em;cursor:pointer;">RELOAD</button>
+      </div>
+      <table class="term" id="market-table"></table>
+    </div>
+
+    <!-- RIGHT: Abrams proprietary indexes -->
+    <div class="term-pane">
+      <h2>Abrams Proprietary Indexes <span class="meta" id="abrams-count">--</span></h2>
+      <div class="abrams-ix-grid" id="abrams-ix-grid"></div>
+    </div>
+
+  </div>
+
+  <p style="color:#6a6457;font-family:var(--mono);font-size:10px;letter-spacing:.15em;margin-top:24px;text-align:center;">
+    Market data via Yahoo Finance (delayed ≥15 min) · Abrams indexes refreshed every 15 min by the YOLO tick · methodology in each card
+  </p>
+</div>
+
+<script src="/static/js/corner-nav.js"></script>
+<script src="/static/js/gamify.js"></script>
+<script>
+function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m])); }
+function fmt(n, decimals = 2) {
+  if (n == null || isNaN(n)) return '—';
+  if (Math.abs(n) >= 1e9) return (n/1e9).toFixed(2) + 'B';
+  if (Math.abs(n) >= 1e6) return (n/1e6).toFixed(2) + 'M';
+  if (Math.abs(n) >= 1e3 && decimals === 0) return n.toLocaleString();
+  return Number(n).toFixed(decimals);
+}
+function sparkSvg(arr, color = '#c9a96e', w = 100, h = 28) {
+  if (!arr || arr.length < 2) return '<svg class="spark"></svg>';
+  const min = Math.min(...arr), max = Math.max(...arr), range = max - min || 1;
+  const pts = arr.map((v, i) => `${(i / (arr.length - 1)) * w},${h - ((v - min) / range) * (h - 4) - 2}`).join(' ');
+  const last = arr[arr.length - 1], first = arr[0];
+  const lineColor = last >= first ? '#4ade80' : '#ef4444';
+  return `<svg class="spark" viewBox="0 0 ${w} ${h}" preserveAspectRatio="none"><polyline points="${pts}" fill="none" stroke="${lineColor}" stroke-width="1.2"/></svg>`;
+}
+
+// ── markets ──────────────────────────────────────────────────────
+async function loadMarket() {
+  const r = await fetch('/api/market').then(x => x.json()).catch(()=>({tickers:[]}));
+  const sortBy = document.getElementById('sort-market').value;
+  const groupBy = document.getElementById('group-market').value;
+  let rows = (r.tickers || []).slice();
+  if (sortBy === 'cat') rows.sort((a,b) => (a.cat||'').localeCompare(b.cat||'') || a.name.localeCompare(b.name));
+  if (sortBy === 'name') rows.sort((a,b) => a.name.localeCompare(b.name));
+  if (sortBy === 'changePct') rows.sort((a,b) => (b.changePct||0) - (a.changePct||0));
+  if (sortBy === 'price') rows.sort((a,b) => (b.price||0) - (a.price||0));
+
+  const head = '<thead><tr><th class="left">Instrument</th><th>Sym</th><th>Last</th><th>Δ</th><th>Δ%</th><th>Spark 1M</th></tr></thead>';
+  let body = '<tbody>';
+  let lastCat = null;
+  for (const t of rows) {
+    if (groupBy === 'cat' && t.cat !== lastCat) {
+      body += `<tr><td colspan="6" class="cat" style="background:#0d0d0d;color:#c9a96e;letter-spacing:.3em;padding:8px 12px;font-size:9px;">${esc(t.cat)}</td></tr>`;
+      lastCat = t.cat;
+    }
+    const dir = t.change > 0 ? 'up' : (t.change < 0 ? 'down' : 'flat');
+    const sign = t.change > 0 ? '+' : '';
+    const decimals = t.cat === 'Bonds' ? 3 : (t.cat === 'Crypto' ? 0 : 2);
+    body += `<tr>
+      <td>${esc(t.name)}</td>
+      <td class="cat">${esc(t.sym)}</td>
+      <td>${fmt(t.price, decimals)}</td>
+      <td class="${dir}">${sign}${fmt(t.change, decimals)}</td>
+      <td class="${dir}">${sign}${fmt(t.changePct, 2)}%</td>
+      <td>${sparkSvg(t.sparkline)}</td>
+    </tr>`;
+  }
+  body += '</tbody>';
+  document.getElementById('market-table').innerHTML = head + body;
+  document.getElementById('market-count').textContent = rows.length + ' INSTRUMENTS';
+  document.getElementById('market-stamp').textContent = r.generated_at ? new Date(r.generated_at).toLocaleTimeString() : '--';
+}
+
+// ── abrams indexes ───────────────────────────────────────────────
+async function loadAbramsIndexes() {
+  const r = await fetch('/api/abrams-indexes').then(x => x.json()).catch(()=>({indexes:[]}));
+  const grid = document.getElementById('abrams-ix-grid');
+  grid.innerHTML = (r.indexes || []).map(ix => {
+    const isWaffle = ix.id === 'WHSI';
+    const waffleCls = isWaffle ? ` whsi ${String(ix.value).toLowerCase()}` : '';
+    const interpCls = /HOT|GREEN|ACCELER|SHIPPING|FILED|INBOUND/i.test(ix.interpretation) ? '' : (/WARM|YELLOW|READY|EARLY SIGNAL|NIGHT|BALANCED/i.test(ix.interpretation) ? 'warn' : (/RED|CRITICAL|COOLING/i.test(ix.interpretation) ? 'err' : ''));
+    const deltaCls = ix.delta > 0 ? 'up' : (ix.delta < 0 ? 'down' : '');
+    const deltaSign = (ix.delta != null && ix.delta > 0) ? '+' : '';
+    return `<div class="abrams-ix${waffleCls}">
+      <div class="head">
+        <span class="id">${esc(ix.id)}</span>
+        <span class="icon">${ix.icon || '◌'}</span>
+      </div>
+      <div class="name">${esc(ix.name)}</div>
+      <div><span class="value" ${ix.color ? `style="color:${esc(ix.color)};"` : ''}>${esc(String(ix.value))}</span><span class="unit">${esc(ix.unit)}</span></div>
+      ${ix.delta != null ? `<div class="delta ${deltaCls}">${deltaSign}${ix.delta} · ${esc(ix.delta_label)}</div>` : `<div class="delta">${esc(ix.delta_label)}</div>`}
+      <div class="interp ${interpCls}">${esc(ix.interpretation)}</div>
+      <div class="method">${esc(ix.method)}</div>
+      ${ix.sparkline ? `<div class="spark-wrap">${sparkSvg(ix.sparkline)}</div>` : ''}
+    </div>`;
+  }).join('');
+  document.getElementById('abrams-count').textContent = (r.indexes||[]).length + ' INDEXES';
+  document.getElementById('abrams-stamp').textContent = r.generated_at ? new Date(r.generated_at).toLocaleTimeString() : '--';
+}
+
+// ── clock ────────────────────────────────────────────────────────
+function tickClock() {
+  const t = new Date();
+  document.getElementById('term-clock').textContent = t.toLocaleTimeString('en-US', { hour12: false });
+}
+
+document.getElementById('sort-market').addEventListener('change', loadMarket);
+document.getElementById('group-market').addEventListener('change', loadMarket);
+
+loadMarket(); loadAbramsIndexes(); tickClock();
+setInterval(tickClock, 1000);
+setInterval(()=>{loadMarket(); loadAbramsIndexes();}, 60_000);
+</script>
+</body>
+</html>
diff --git a/scripts/abrams-indexes.js b/scripts/abrams-indexes.js
new file mode 100755
index 00000000..060c24e5
--- /dev/null
+++ b/scripts/abrams-indexes.js
@@ -0,0 +1,338 @@
+#!/usr/bin/env node
+// abrams-indexes.js — computes 8 proprietary Abrams indexes from live local data.
+// Spiritual cousins to FEMA's Waffle House Index + the Pentagon Pizza Index — quirky,
+// real, observable. Writes data/abrams-indexes.json with current values + 7-day
+// sparklines + methodology blurbs.
+
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+
+const ROOT = path.resolve(__dirname, '..');
+const HOME = process.env.HOME;
+const PROJECTS = path.join(HOME, 'Projects');
+const OUT = path.join(ROOT, 'data', 'abrams-indexes.json');
+const LOG = path.join(ROOT, 'data', 'build-log.jsonl');
+const log = (type, payload) => fs.appendFileSync(LOG, JSON.stringify({ ts: new Date().toISOString(), type, ...payload }) + '\n');
+
+const exec = (cmd) => { try { return execSync(cmd, { encoding: 'utf8', stdio: ['pipe','pipe','ignore'], maxBuffer: 20 * 1024 * 1024 }).trim(); } catch { return ''; } };
+const readJson = (p, fb) => { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return fb; } };
+
+// ── enumerate abrams projects on disk ─────────────────────────────────
+function abramsDirs() {
+  try {
+    return fs.readdirSync(PROJECTS, { withFileTypes: true })
+      .filter(d => d.isDirectory())
+      .filter(d => /abrams/i.test(d.name))
+      .map(d => path.join(PROJECTS, d.name));
+  } catch { return []; }
+}
+
+// ── helper: commits per day, last 30 days, across all abrams projects ─
+function commitTimeline(days = 30) {
+  const since = Date.now() - days * 86400000;
+  const dirs = abramsDirs();
+  const buckets = {}; // ISO day → count
+  for (const d of dirs) {
+    if (!fs.existsSync(path.join(d, '.git'))) continue;
+    const log = exec(`cd "${d}" && git log --since="${days} days ago" --format="%cI" 2>/dev/null`);
+    log.split('\n').filter(Boolean).forEach(iso => {
+      const day = iso.slice(0, 10);
+      buckets[day] = (buckets[day] || 0) + 1;
+    });
+  }
+  // dense series for the last `days` days
+  const series = [];
+  for (let i = days - 1; i >= 0; i--) {
+    const day = new Date(Date.now() - i * 86400000).toISOString().slice(0, 10);
+    series.push({ day, count: buckets[day] || 0 });
+  }
+  return series;
+}
+
+// ── helper: late-night commits (22:00-02:59 local) over N days ────────
+function lateNightCommits(days = 30) {
+  const dirs = abramsDirs();
+  const all = [];
+  for (const d of dirs) {
+    if (!fs.existsSync(path.join(d, '.git'))) continue;
+    const log = exec(`cd "${d}" && git log --since="${days} days ago" --format="%cI" 2>/dev/null`);
+    log.split('\n').filter(Boolean).forEach(iso => {
+      const h = new Date(iso).getHours();
+      if (h >= 22 || h < 3) all.push(iso);
+    });
+  }
+  return all;
+}
+
+// ── helper: pm2 status across the fleet ───────────────────────────────
+function pm2Status() {
+  try {
+    const raw = exec('pm2 jlist 2>/dev/null');
+    // pm2 jlist sometimes prints initialization noise on the first line; isolate the JSON array
+    const start = raw.indexOf('[');
+    const end = raw.lastIndexOf(']');
+    if (start < 0 || end < 0) throw new Error('no JSON array');
+    const ps = JSON.parse(raw.slice(start, end + 1));
+    const all = ps.length;
+    const online = ps.filter(p => p.pm2_env?.status === 'online').length;
+    const restarting = ps.filter(p => (p.pm2_env?.unstable_restarts || 0) > 0).length;
+    return { all, online, restarting, down: all - online };
+  } catch { return { all: 0, online: 0, restarting: 0, down: 0 }; }
+}
+
+// ── helper: LOC across the empire ─────────────────────────────────────
+function totalLoc() {
+  const dirs = abramsDirs();
+  let total = 0;
+  for (const d of dirs) {
+    const out = exec(`find "${d}" -type f \\( -name "*.js" -o -name "*.ts" -o -name "*.tsx" -o -name "*.html" -o -name "*.css" -o -name "*.py" -o -name "*.sh" -o -name "*.md" \\) -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null | xargs wc -l 2>/dev/null | tail -1 | awk '{print $1}'`);
+    total += parseInt(out, 10) || 0;
+  }
+  return total;
+}
+
+// ── helper: sparkline generator (last 7 buckets of value) ─────────────
+function spark7(timeline) {
+  // timeline is array of {day, count}; take last 7 days
+  return timeline.slice(-7).map(x => x.count);
+}
+
+// ── index 1 — Abrams Build Index (commits/day, normalized to 100 base on 7d avg) ──
+function buildIndex() {
+  const t30 = commitTimeline(30);
+  const last7 = t30.slice(-7);
+  const prev7 = t30.slice(-14, -7);
+  const avg = a => a.length ? a.reduce((s,x)=>s+x.count,0)/a.length : 0;
+  const cur = avg(last7);
+  const prev = avg(prev7) || 1;
+  return {
+    id: 'ABI',
+    name: 'Abrams Build Index',
+    icon: '◐',
+    value: Math.round((cur / prev) * 100),
+    unit: 'idx',
+    delta: Number((cur - avg(prev7)).toFixed(2)),
+    delta_label: 'commits/day vs prev 7d',
+    sparkline: spark7(t30),
+    method: 'Daily commit count across all Abrams projects, indexed 100 = previous 7-day average. >100 = accelerating. <100 = cooling.',
+    interpretation: cur > avg(prev7) ? 'BUILD ACCELERATING' : cur < avg(prev7) * 0.7 ? 'BUILD COOLING' : 'BUILD STEADY',
+  };
+}
+
+// ── index 2 — Operator Heat Index (composite 0-100) ───────────────────
+function operatorHeatIndex() {
+  const t = commitTimeline(7);
+  const today = t[t.length - 1]?.count || 0;
+  const avg7 = t.reduce((s,x)=>s+x.count, 0) / 7;
+  const pm2 = pm2Status();
+  const pm2Score = pm2.all ? (pm2.online / pm2.all) * 50 : 0; // 50pt for full uptime
+  const commitScore = Math.min(50, today * 5); // 50pt cap on commit velocity
+  const value = Math.round(pm2Score + commitScore);
+  return {
+    id: 'OHI',
+    name: 'Operator Heat Index',
+    icon: '✸',
+    value,
+    unit: '/100',
+    delta: today - Math.round(avg7),
+    delta_label: 'commits today vs 7d avg',
+    sparkline: spark7(t),
+    method: 'Composite: 50pt for pm2 uptime ratio + 50pt cap for today commits × 5. Reads founder activity intensity.',
+    interpretation: value > 70 ? 'HOT' : value > 40 ? 'WARM' : 'COLD',
+  };
+}
+
+// ── index 3 — Late-Night Code Index (% of last-7-day commits in 22:00-02:59) ──
+function lateNightIndex() {
+  const dirs = abramsDirs();
+  let totalRecent = 0, lateRecent = 0;
+  const dailyLate = {};
+  for (const d of dirs) {
+    if (!fs.existsSync(path.join(d, '.git'))) continue;
+    const log = exec(`cd "${d}" && git log --since="7 days ago" --format="%cI" 2>/dev/null`);
+    log.split('\n').filter(Boolean).forEach(iso => {
+      totalRecent++;
+      const h = new Date(iso).getHours();
+      if (h >= 22 || h < 3) {
+        lateRecent++;
+        const day = iso.slice(0, 10);
+        dailyLate[day] = (dailyLate[day] || 0) + 1;
+      }
+    });
+  }
+  const pct = totalRecent ? Math.round((lateRecent / totalRecent) * 100) : 0;
+  // build 7-day sparkline
+  const series = [];
+  for (let i = 6; i >= 0; i--) {
+    const day = new Date(Date.now() - i * 86400000).toISOString().slice(0, 10);
+    series.push(dailyLate[day] || 0);
+  }
+  return {
+    id: 'LNCI',
+    name: 'Late-Night Code Index',
+    icon: '☾',
+    value: pct,
+    unit: '%',
+    delta: lateRecent,
+    delta_label: 'late-night commits in 7d',
+    sparkline: series,
+    method: '% of last-7-day commits landing between 22:00-02:59 local time. Productivity-vs-burnout signal — sustained >40% reads "founder is running hot."',
+    interpretation: pct > 40 ? '🔥 BURNING THE MIDNIGHT OIL' : pct > 20 ? 'BALANCED' : 'DAYTIME OPERATOR',
+  };
+}
+
+// ── index 4 — Abrams Pizza Index (count of late-night commits in 7d) ──
+// (Spiritual cousin to the Pentagon Pizza Index — when commits spike late, something is shipping.)
+function pizzaIndex() {
+  const late = lateNightCommits(7);
+  const value = late.length;
+  // sparkline = late commits per day for last 7 days
+  const daily = {};
+  for (const iso of late) {
+    const day = iso.slice(0, 10);
+    daily[day] = (daily[day] || 0) + 1;
+  }
+  const series = [];
+  for (let i = 6; i >= 0; i--) {
+    const day = new Date(Date.now() - i * 86400000).toISOString().slice(0, 10);
+    series.push(daily[day] || 0);
+  }
+  return {
+    id: 'API',
+    name: 'Abrams Pizza Index',
+    icon: '🍕',
+    value,
+    unit: 'late-night commits / 7d',
+    delta: series[series.length - 1] - (series[series.length - 2] || 0),
+    delta_label: 'last 24h vs prior day',
+    sparkline: series,
+    method: 'Direct count of commits landing 22:00-02:59 local in the last 7 days. The original Pentagon Pizza Index tracks pizza orders to DOD; the Abrams variant tracks late-night commits. Spikes precede shipped features.',
+    interpretation: value > 15 ? 'SHIPPING IMMINENT' : value > 5 ? 'NIGHT BUILD ACTIVE' : 'CALM',
+  };
+}
+
+// ── index 5 — Waffle House Stack Index (green/yellow/red) ────────────
+function waffleHouseIndex() {
+  const pm2 = pm2Status();
+  const ratio = pm2.all ? pm2.online / pm2.all : 1;
+  let value, color, interpretation;
+  if (ratio === 1) { value = 'GREEN'; color = '#4ade80'; interpretation = 'FULL MENU — all services online'; }
+  else if (ratio >= 0.8) { value = 'YELLOW'; color = '#f59e0b'; interpretation = 'LIMITED MENU — degraded operations'; }
+  else if (ratio >= 0.5) { value = 'ORANGE'; color = '#fb923c'; interpretation = 'GRILL OFF — major outage'; }
+  else { value = 'RED'; color = '#ef4444'; interpretation = 'CLOSED — critical infrastructure down'; }
+  return {
+    id: 'WHSI',
+    name: 'Waffle House Stack Index',
+    icon: '🧇',
+    value,
+    color,
+    unit: pm2.online + '/' + pm2.all,
+    delta: pm2.down ? -pm2.down : 0,
+    delta_label: pm2.down ? `${pm2.down} down · ${pm2.restarting} restarting` : 'all systems operational',
+    sparkline: null,
+    method: "FEMA's Waffle House Index measures disaster severity by whether Waffle House is serving. The Abrams variant: GREEN if every pm2 process online, YELLOW if 80-99%, ORANGE if 50-79%, RED if <50%. Real-time stack health.",
+    interpretation,
+  };
+}
+
+// ── index 6 — Lead Velocity Index (leads/week, 7-day rolling) ─────────
+function leadVelocityIndex() {
+  const leadsFile = path.join(ROOT, 'data', 'leads.jsonl');
+  let leads = [];
+  try { leads = fs.readFileSync(leadsFile, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l)); } catch {}
+  const now = Date.now();
+  const last7 = leads.filter(l => now - new Date(l.ts).getTime() < 7 * 86400000).length;
+  const prev7 = leads.filter(l => {
+    const d = now - new Date(l.ts).getTime();
+    return d < 14 * 86400000 && d >= 7 * 86400000;
+  }).length;
+  // 7-day daily sparkline
+  const daily = {};
+  for (const l of leads) {
+    const day = (l.ts || '').slice(0, 10);
+    if (day) daily[day] = (daily[day] || 0) + 1;
+  }
+  const series = [];
+  for (let i = 6; i >= 0; i--) {
+    const day = new Date(Date.now() - i * 86400000).toISOString().slice(0, 10);
+    series.push(daily[day] || 0);
+  }
+  return {
+    id: 'LVI',
+    name: 'Lead Velocity Index',
+    icon: '⟶',
+    value: last7,
+    unit: 'leads / 7d',
+    delta: last7 - prev7,
+    delta_label: 'vs prior 7d',
+    sparkline: series,
+    method: 'Leads captured in the last 7 days via /api/lead. Compare to prior 7d for momentum. Baseline build phase = 0-1; growth phase = 5+.',
+    interpretation: last7 >= 5 ? 'INBOUND PIPELINE' : last7 >= 1 ? 'EARLY SIGNAL' : 'PRE-LAUNCH',
+  };
+}
+
+// ── index 7 — Trademark Confidence Index (qualitative score) ──────────
+function tmConfidenceIndex() {
+  const tm = readJson(path.join(ROOT, 'data', 'trademark.json'), null);
+  if (!tm) return null;
+  let score = 0;
+  const checks = [
+    { rule: 'Mark identified',          ok: !!tm.mark,                              pts: 10 },
+    { rule: 'Owner identified',         ok: !!tm.owner,                             pts: 10 },
+    { rule: '5 classes selected',       ok: (tm.recommended_classes || []).length >= 5, pts: 20 },
+    { rule: 'Filing routes researched', ok: (tm.filing_routes || []).length >= 3,   pts: 15 },
+    { rule: 'Domain status assessed',   ok: !!tm.domain_status,                     pts: 10 },
+    { rule: 'Common-law rights noted',  ok: !!tm.common_law_status,                 pts: 10 },
+    { rule: 'Next-action plan in place', ok: (tm.next_actions || []).length >= 3,   pts: 10 },
+    { rule: 'Filing submitted to USPTO', ok: false,                                 pts: 15 }, // TODO when filed
+  ];
+  for (const c of checks) if (c.ok) score += c.pts;
+  return {
+    id: 'TMCI',
+    name: 'TM Confidence Index',
+    icon: '®',
+    value: score,
+    unit: '/100',
+    delta: 0,
+    delta_label: `${checks.filter(c => c.ok).length}/${checks.length} steps complete`,
+    sparkline: null,
+    method: 'Weighted checklist: mark+owner identified (20pt), 5+ classes selected (20), filing routes researched (15), domain status (10), common-law noted (10), next-action plan (10), USPTO filing submitted (15).',
+    interpretation: score >= 85 ? 'FILED' : score >= 60 ? 'READY TO FILE' : score >= 30 ? 'IN PREP' : 'EARLY',
+  };
+}
+
+// ── index 8 — Empire Sprawl Index (total LOC across portfolio) ────────
+function empireSprawlIndex() {
+  const loc = totalLoc();
+  return {
+    id: 'ESI',
+    name: 'Empire Sprawl Index',
+    icon: '✦',
+    value: loc,
+    unit: 'LOC',
+    delta: null,
+    delta_label: `across ${abramsDirs().length} projects`,
+    sparkline: null,
+    method: 'Total lines of code across every Abrams project (.js / .ts / .tsx / .html / .css / .py / .sh / .md, excluding node_modules + .git). One scalar for the size of the empire.',
+    interpretation: loc > 100000 ? 'SOLO-FOUNDER MEGAPROJECT' : loc > 25000 ? 'MID-EMPIRE' : 'EARLY',
+  };
+}
+
+// ── run ───────────────────────────────────────────────────────────────
+const indexes = [
+  buildIndex(),
+  operatorHeatIndex(),
+  lateNightIndex(),
+  pizzaIndex(),
+  waffleHouseIndex(),
+  leadVelocityIndex(),
+  tmConfidenceIndex(),
+  empireSprawlIndex(),
+].filter(Boolean);
+
+const out = { generated_at: new Date().toISOString(), indexes };
+fs.writeFileSync(OUT, JSON.stringify(out, null, 2));
+log('abrams-indexes', { count: indexes.length });
+console.log(`abrams-indexes: ${indexes.length} indexes → ${OUT}`);
+for (const ix of indexes) console.log(`  ${ix.id.padEnd(6)} ${String(ix.value).padEnd(8)} ${ix.unit.padEnd(20)} ${ix.interpretation}`);
diff --git a/scripts/market-data.js b/scripts/market-data.js
new file mode 100755
index 00000000..74208907
--- /dev/null
+++ b/scripts/market-data.js
@@ -0,0 +1,99 @@
+#!/usr/bin/env node
+// market-data.js — Bloomberg-style index/bond/commodity/crypto snapshot for the Abrams Terminal.
+// Uses Yahoo Finance's public spark endpoint (no auth, free).
+// Falls back to Stooq if Yahoo errors. Writes data/market.json.
+
+const https = require('https');
+const fs = require('fs');
+const path = require('path');
+
+const ROOT = path.resolve(__dirname, '..');
+const OUT = path.join(ROOT, 'data', 'market.json');
+const LOG = path.join(ROOT, 'data', 'build-log.jsonl');
+const log = (type, payload) => fs.appendFileSync(LOG, JSON.stringify({ ts: new Date().toISOString(), type, ...payload }) + '\n');
+
+const TICKERS = [
+  { sym: 'SPY',     name: 'S&P 500 ETF',         cat: 'Stocks' },
+  { sym: 'QQQ',     name: 'Nasdaq-100 ETF',      cat: 'Stocks' },
+  { sym: 'DIA',     name: 'Dow Industrials ETF', cat: 'Stocks' },
+  { sym: 'IWM',     name: 'Russell 2000 ETF',    cat: 'Stocks' },
+  { sym: '^VIX',    name: 'CBOE Volatility',     cat: 'Stocks' },
+  { sym: '^TNX',    name: '10-Year Treasury',    cat: 'Bonds'  },
+  { sym: '^TYX',    name: '30-Year Treasury',    cat: 'Bonds'  },
+  { sym: '^IRX',    name: '13-Week T-Bill',      cat: 'Bonds'  },
+  { sym: 'GC=F',    name: 'Gold Futures',        cat: 'Commodities' },
+  { sym: 'CL=F',    name: 'WTI Crude Oil',       cat: 'Commodities' },
+  { sym: 'BTC-USD', name: 'Bitcoin',             cat: 'Crypto' },
+];
+
+function fetchJson(url, timeoutMs = 4000) {
+  return new Promise(resolve => {
+    const req = https.get(url, {
+      timeout: timeoutMs,
+      headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_0) AbramsTerminal/1.0' }
+    }, r => {
+      let data = '';
+      r.on('data', c => data += c);
+      r.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve(null); } });
+    });
+    req.on('error', () => resolve(null));
+    req.on('timeout', () => { req.destroy(); resolve(null); });
+  });
+}
+
+async function fetchSpark(symbol) {
+  // Yahoo "spark" endpoint — free, no auth. Gives us last ~30 days of closes.
+  const url = `https://query1.finance.yahoo.com/v7/finance/spark?symbols=${encodeURIComponent(symbol)}&range=1mo&interval=1d&indicators=close&includeTimestamps=false&includePrePost=false`;
+  const j = await fetchJson(url);
+  const result = j?.spark?.result?.[0]?.response?.[0];
+  if (!result) return null;
+  const closes = (result.indicators?.quote?.[0]?.close || []).filter(x => x != null);
+  const meta = result.meta || {};
+  return {
+    symbol,
+    price: meta.regularMarketPrice ?? closes[closes.length - 1] ?? null,
+    previousClose: meta.previousClose ?? meta.chartPreviousClose ?? closes[closes.length - 2] ?? null,
+    dayHigh: meta.regularMarketDayHigh ?? null,
+    dayLow: meta.regularMarketDayLow ?? null,
+    fiftyTwoWeekHigh: meta.fiftyTwoWeekHigh ?? null,
+    fiftyTwoWeekLow: meta.fiftyTwoWeekLow ?? null,
+    sparkline: closes,
+    currency: meta.currency ?? 'USD',
+    exchangeName: meta.exchangeName ?? '',
+  };
+}
+
+async function main() {
+  const out = { generated_at: new Date().toISOString(), tickers: [] };
+  let okCount = 0, errCount = 0;
+  for (const t of TICKERS) {
+    try {
+      const d = await fetchSpark(t.sym);
+      if (d && d.price != null) {
+        const change = d.previousClose ? d.price - d.previousClose : 0;
+        const changePct = d.previousClose ? (change / d.previousClose) * 100 : 0;
+        out.tickers.push({
+          ...t,
+          ...d,
+          change: Number(change.toFixed(4)),
+          changePct: Number(changePct.toFixed(4)),
+          updated_at: new Date().toISOString(),
+        });
+        okCount++;
+      } else {
+        out.tickers.push({ ...t, error: 'fetch failed', sparkline: [] });
+        errCount++;
+      }
+    } catch (e) {
+      out.tickers.push({ ...t, error: String(e.message || e), sparkline: [] });
+      errCount++;
+    }
+    // courtesy delay so Yahoo doesn't throttle
+    await new Promise(r => setTimeout(r, 120));
+  }
+  fs.writeFileSync(OUT, JSON.stringify(out, null, 2));
+  log('market-data', { ok: okCount, err: errCount, total: TICKERS.length });
+  console.log(`market: ${okCount}/${TICKERS.length} tickers → ${OUT}`);
+}
+
+main().catch(e => { console.error(e); process.exit(1); });
diff --git a/scripts/tick.sh b/scripts/tick.sh
index bc3023da..a745f417 100755
--- a/scripts/tick.sh
+++ b/scripts/tick.sh
@@ -43,6 +43,16 @@ if node scripts/counters.js 2>>data/err.log; then
   emit "counters-ok" "\"phase\":\"counters\""
 fi
 
+# 3.6) refresh market data (Yahoo)
+if node scripts/market-data.js 2>>data/err.log; then
+  emit "market-ok" "\"phase\":\"market\""
+fi
+
+# 3.7) refresh Abrams proprietary indexes
+if node scripts/abrams-indexes.js 2>>data/err.log; then
+  emit "abrams-idx-ok" "\"phase\":\"abrams-indexes\""
+fi
+
 # 4) re-audit Mag-20
 if node scripts/mag-20-audit.js 2>>data/err.log; then
   emit "mag-20-ok" "\"phase\":\"audit\""
diff --git a/server.js b/server.js
index de0be4eb..9a82238d 100644
--- a/server.js
+++ b/server.js
@@ -57,6 +57,8 @@ app.get('/api/ideas', (_, res) => res.json(readJson(IDEAS_FILE, { ideas: [], upd
 app.get('/api/trademark', (_, res) => res.json(readJson(TRADEMARK_FILE, {})));
 app.get('/api/counters', (_, res) => res.json(readJson(path.join(DATA, 'counters.json'), { counters: [] })));
 app.get('/api/info-hub', (_, res) => res.json(readJson(path.join(DATA, 'info-hub.json'), { sources: [] })));
+app.get('/api/market', (_, res) => res.json(readJson(path.join(DATA, 'market.json'), { tickers: [], generated_at: null })));
+app.get('/api/abrams-indexes', (_, res) => res.json(readJson(path.join(DATA, 'abrams-indexes.json'), { indexes: [], generated_at: null })));
 app.get('/api/competitors', (req, res) => {
   try { res.type('text/markdown').send(fs.readFileSync(path.join(DATA, 'competitors.md'), 'utf8')); }
   catch { res.status(404).json({ error: 'competitors analysis not yet generated' }); }
@@ -99,6 +101,7 @@ app.get('/mag-20', (_, res) => res.sendFile(path.join(ROOT, 'public', 'mag-20.ht
 app.get('/press', (_, res) => res.sendFile(path.join(ROOT, 'public', 'press.html')));
 app.get('/hub', (_, res) => res.sendFile(path.join(ROOT, 'public', 'hub.html')));
 app.get('/competitors', (_, res) => res.sendFile(path.join(ROOT, 'public', 'competitors.html')));
+app.get('/terminal', (_, res) => res.sendFile(path.join(ROOT, 'public', 'terminal.html')));
 app.get('/api/build-log', (req, res) => res.json({ events: readJsonl(BUILD_LOG, parseInt(req.query.n || '50', 10)) }));
 app.get('/api/wall', (req, res) => res.json({ posts: readJsonl(WALL_FILE, parseInt(req.query.n || '100', 10)) }));
 

← 6a97ad43 tick: refresh inventory + SWOT at 2026-05-14T16:32:06Z  ·  back to Abrams  ·  tick: refresh inventory + SWOT at 2026-05-14T16:47:08Z 191ac34b →