← 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
A data/abrams-indexes.jsonM data/build-log.jsonlM data/counters.jsonM data/mag-20-results.jsonA data/market.jsonM public/index.htmlM public/js/corner-nav.jsA public/terminal.htmlA scripts/abrams-indexes.jsA scripts/market-data.jsM scripts/tick.shM server.js
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 + ' · ' + 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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 →