@blockrun/franklin 3.3.3 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +55 -4
  2. package/dist/agent/commands.d.ts +1 -1
  3. package/dist/agent/commands.js +128 -17
  4. package/dist/agent/compact.d.ts +2 -2
  5. package/dist/agent/compact.js +148 -22
  6. package/dist/agent/context.d.ts +8 -3
  7. package/dist/agent/context.js +301 -108
  8. package/dist/agent/error-classifier.d.ts +11 -2
  9. package/dist/agent/error-classifier.js +64 -10
  10. package/dist/agent/llm.d.ts +8 -1
  11. package/dist/agent/llm.js +114 -19
  12. package/dist/agent/loop.d.ts +1 -2
  13. package/dist/agent/loop.js +509 -61
  14. package/dist/agent/optimize.d.ts +2 -2
  15. package/dist/agent/optimize.js +9 -7
  16. package/dist/agent/permissions.d.ts +1 -1
  17. package/dist/agent/permissions.js +1 -1
  18. package/dist/agent/planner.d.ts +42 -0
  19. package/dist/agent/planner.js +110 -0
  20. package/dist/agent/reduce.d.ts +7 -1
  21. package/dist/agent/reduce.js +85 -3
  22. package/dist/agent/streaming-executor.d.ts +6 -1
  23. package/dist/agent/streaming-executor.js +83 -5
  24. package/dist/agent/tokens.d.ts +11 -2
  25. package/dist/agent/tokens.js +38 -5
  26. package/dist/agent/tool-guard.d.ts +27 -0
  27. package/dist/agent/tool-guard.js +324 -0
  28. package/dist/agent/types.d.ts +7 -1
  29. package/dist/agent/types.js +1 -1
  30. package/dist/brain/extract.d.ts +11 -0
  31. package/dist/brain/extract.js +154 -0
  32. package/dist/brain/index.d.ts +3 -0
  33. package/dist/brain/index.js +2 -0
  34. package/dist/brain/store.d.ts +42 -0
  35. package/dist/brain/store.js +225 -0
  36. package/dist/brain/types.d.ts +45 -0
  37. package/dist/brain/types.js +5 -0
  38. package/dist/commands/daemon.js +2 -1
  39. package/dist/commands/start.js +16 -3
  40. package/dist/config.js +1 -1
  41. package/dist/index.js +27 -2
  42. package/dist/learnings/extractor.d.ts +13 -0
  43. package/dist/learnings/extractor.js +69 -8
  44. package/dist/learnings/index.d.ts +1 -1
  45. package/dist/learnings/index.js +1 -1
  46. package/dist/learnings/store.js +42 -13
  47. package/dist/learnings/types.d.ts +1 -1
  48. package/dist/mcp/client.d.ts +1 -1
  49. package/dist/mcp/client.js +5 -5
  50. package/dist/mcp/config.d.ts +1 -1
  51. package/dist/mcp/config.js +1 -1
  52. package/dist/panel/html.d.ts +2 -0
  53. package/dist/panel/html.js +409 -146
  54. package/dist/panel/server.js +19 -0
  55. package/dist/pricing.js +3 -2
  56. package/dist/proxy/fallback.d.ts +3 -1
  57. package/dist/proxy/fallback.js +4 -4
  58. package/dist/proxy/server.js +29 -11
  59. package/dist/proxy/sse-translator.js +1 -1
  60. package/dist/router/categories.d.ts +21 -0
  61. package/dist/router/categories.js +96 -0
  62. package/dist/router/index.d.ts +9 -2
  63. package/dist/router/index.js +106 -27
  64. package/dist/router/local-elo.d.ts +32 -0
  65. package/dist/router/local-elo.js +107 -0
  66. package/dist/router/selector.d.ts +46 -0
  67. package/dist/router/selector.js +106 -0
  68. package/dist/session/storage.d.ts +5 -1
  69. package/dist/session/storage.js +24 -2
  70. package/dist/social/a11y.d.ts +1 -1
  71. package/dist/social/a11y.js +5 -1
  72. package/dist/social/browser.d.ts +5 -0
  73. package/dist/social/browser.js +22 -0
  74. package/dist/social/preflight.d.ts +4 -0
  75. package/dist/social/preflight.js +42 -3
  76. package/dist/stats/failures.d.ts +20 -0
  77. package/dist/stats/failures.js +63 -0
  78. package/dist/stats/format.d.ts +6 -0
  79. package/dist/stats/format.js +23 -0
  80. package/dist/stats/insights.js +1 -21
  81. package/dist/stats/session-tracker.d.ts +21 -0
  82. package/dist/stats/session-tracker.js +28 -0
  83. package/dist/stats/tracker.d.ts +1 -1
  84. package/dist/stats/tracker.js +1 -1
  85. package/dist/tools/bash.d.ts +14 -1
  86. package/dist/tools/bash.js +132 -7
  87. package/dist/tools/edit.js +77 -14
  88. package/dist/tools/glob.js +13 -3
  89. package/dist/tools/grep.js +30 -12
  90. package/dist/tools/imagegen.js +3 -3
  91. package/dist/tools/index.d.ts +1 -1
  92. package/dist/tools/index.js +5 -1
  93. package/dist/tools/read.d.ts +16 -2
  94. package/dist/tools/read.js +36 -8
  95. package/dist/tools/searchx.d.ts +6 -2
  96. package/dist/tools/searchx.js +221 -44
  97. package/dist/tools/subagent.js +37 -3
  98. package/dist/tools/task.js +43 -7
  99. package/dist/tools/validate.d.ts +11 -0
  100. package/dist/tools/validate.js +42 -0
  101. package/dist/tools/webfetch.js +18 -7
  102. package/dist/tools/websearch.js +41 -7
  103. package/dist/tools/write.js +26 -6
  104. package/dist/ui/app.js +31 -6
  105. package/dist/ui/model-picker.d.ts +1 -1
  106. package/dist/ui/model-picker.js +1 -1
  107. package/dist/ui/terminal.d.ts +1 -1
  108. package/dist/ui/terminal.js +1 -1
  109. package/package.json +2 -2
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Franklin Panel — embedded HTML dashboard.
3
3
  * Single page, dark theme, zero dependencies.
4
+ * Design language adapted from Multica (oklch palette, sidebar nav).
5
+ * Currency-grade watermark + Inter font.
4
6
  */
5
7
  export function getHTML() {
6
8
  return `<!DOCTYPE html>
@@ -9,147 +11,402 @@ export function getHTML() {
9
11
  <meta charset="utf-8">
10
12
  <meta name="viewport" content="width=device-width, initial-scale=1">
11
13
  <title>Franklin Panel</title>
14
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='30' y='20' width='55' height='60' rx='14' stroke='white' stroke-width='8' fill='none'/%3E%3Cpath d='M15 35 L25 35' stroke='white' stroke-width='6' stroke-linecap='round'/%3E%3Cpath d='M10 50 L25 50' stroke='white' stroke-width='6' stroke-linecap='round'/%3E%3Cpath d='M15 65 L25 65' stroke='white' stroke-width='6' stroke-linecap='round'/%3E%3C/svg%3E">
15
+ <link rel="preconnect" href="https://fonts.googleapis.com">
16
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
17
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600;700;800&display=swap" rel="stylesheet">
12
18
  <style>
13
19
  :root {
14
- --bg: #0a0a0f;
15
- --bg-card: #12121a;
16
- --bg-hover: #1a1a2a;
17
- --border: #2a2a3a;
18
- --text: #e0e0e8;
19
- --text-dim: #6a6a7a;
20
- --accent: #10b981;
21
- --gold: #ffd700;
22
- --blue: #60a5fa;
23
- --danger: #ef4444;
24
- --mono: 'SF Mono','Fira Code','Cascadia Code','Menlo',monospace;
25
- --sans: -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
20
+ --bg: oklch(0.13 0.006 286);
21
+ --bg-card: oklch(0.19 0.006 286);
22
+ --bg-card-hover: oklch(0.23 0.006 286);
23
+ --bg-sidebar: oklch(0.16 0.005 286);
24
+ --border: oklch(1 0 0 / 8%);
25
+ --border-strong: oklch(1 0 0 / 14%);
26
+ --text: oklch(0.96 0 0);
27
+ --text-dim: oklch(0.50 0.012 286);
28
+ --text-muted: oklch(0.68 0.012 286);
29
+ --brand: oklch(0.68 0.16 260);
30
+ --success: oklch(0.72 0.17 150);
31
+ --warning: oklch(0.78 0.14 85);
32
+ --danger: oklch(0.65 0.20 25);
33
+ --gold: oklch(0.85 0.13 85);
34
+ --gold-dim: oklch(0.45 0.08 85);
35
+ --mono: 'JetBrains Mono','SF Mono','Fira Code','Menlo',monospace;
36
+ --sans: 'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
37
+ --radius: 10px;
26
38
  }
27
39
  * { margin:0; padding:0; box-sizing:border-box; }
28
- body { background:var(--bg); color:var(--text); font-family:var(--sans); font-size:14px; }
29
- a { color:var(--blue); text-decoration:none; }
40
+ body { background:var(--bg); color:var(--text); font-family:var(--sans); font-size:14px; display:flex; height:100vh; overflow:hidden; -webkit-font-smoothing:antialiased; }
41
+ a { color:var(--brand); text-decoration:none; }
30
42
  a:hover { text-decoration:underline; }
43
+ ::-webkit-scrollbar { width:5px; }
44
+ ::-webkit-scrollbar-track { background:transparent; }
45
+ ::-webkit-scrollbar-thumb { background:oklch(1 0 0 / 6%); border-radius:3px; }
46
+ ::-webkit-scrollbar-thumb:hover { background:oklch(1 0 0 / 14%); }
31
47
 
32
- header {
33
- display:flex; align-items:center; justify-content:space-between;
34
- padding:16px 24px; border-bottom:1px solid var(--border);
48
+ /* ── Sidebar ── */
49
+ .sidebar {
50
+ width:230px; min-width:230px; background:var(--bg-sidebar);
51
+ border-right:1px solid var(--border); display:flex; flex-direction:column;
52
+ padding:20px 0; overflow-y:auto;
35
53
  }
36
- header h1 { font-size:18px; font-weight:600; }
37
- header h1 span { color:var(--gold); }
38
- .dot { width:8px; height:8px; border-radius:50%; display:inline-block; margin-left:8px; }
39
- .dot.on { background:var(--accent); animation:pulse 2s infinite; }
54
+ .sidebar-header { padding:0 20px 24px; }
55
+ .sidebar-brand { display:flex; align-items:center; gap:10px; margin-bottom:2px; }
56
+ .sidebar-brand .icon {
57
+ width:32px; height:32px; border-radius:50%; overflow:hidden;
58
+ border:1px solid oklch(0.85 0.13 85 / 30%); flex-shrink:0;
59
+ }
60
+ .sidebar-brand .icon img { width:100%; height:100%; object-fit:cover; object-position:top; }
61
+ .sidebar-brand h1 { font-size:16px; font-weight:700; letter-spacing:-0.02em; }
62
+ .sidebar-sub { font-size:10px; color:var(--text-dim); margin-left:38px; margin-top:-1px; letter-spacing:0.3px; }
63
+ .sidebar-status {
64
+ display:flex; align-items:center; gap:6px; margin-left:38px; margin-top:8px;
65
+ font-size:10px; color:var(--text-dim); font-family:var(--mono);
66
+ }
67
+ .dot { width:6px; height:6px; border-radius:50%; }
68
+ .dot.on { background:var(--success); box-shadow:0 0 8px oklch(0.72 0.17 150 / 60%); }
40
69
  .dot.off { background:var(--danger); }
41
- @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.5} }
42
70
 
43
- nav { display:flex; gap:0; border-bottom:1px solid var(--border); padding:0 24px; }
44
- nav button {
45
- background:none; border:none; color:var(--text-dim); padding:12px 20px;
46
- cursor:pointer; font-size:14px; border-bottom:2px solid transparent;
47
- transition:all .15s;
71
+ .sidebar-label {
72
+ font-size:10px; font-weight:600; color:var(--text-dim);
73
+ text-transform:uppercase; letter-spacing:0.8px;
74
+ padding:20px 20px 8px; user-select:none;
75
+ }
76
+ .sidebar-nav { display:flex; flex-direction:column; gap:1px; padding:0 10px; }
77
+ .nav-item {
78
+ display:flex; align-items:center; gap:10px;
79
+ padding:9px 14px; border-radius:8px;
80
+ cursor:pointer; color:var(--text-muted); font-size:13px; font-weight:500;
81
+ border:none; background:none; width:100%; text-align:left;
82
+ transition:all .15s ease;
83
+ }
84
+ .nav-item:hover { background:oklch(1 0 0 / 5%); color:var(--text); }
85
+ .nav-item.active { background:oklch(1 0 0 / 8%); color:var(--text); }
86
+ .nav-item svg { width:16px; height:16px; opacity:0.5; flex-shrink:0; }
87
+ .nav-item.active svg { opacity:0.9; }
88
+
89
+ .sidebar-footer {
90
+ margin-top:auto; padding:16px 20px; border-top:1px solid var(--border);
91
+ }
92
+ .wallet-mini { font-family:var(--mono); font-size:11px; color:var(--text-dim); }
93
+ .wallet-mini .bal { color:var(--gold); font-weight:700; font-size:14px; display:block; margin-bottom:3px; }
94
+
95
+ /* ── Content ── */
96
+ .content { flex:1; overflow-y:auto; padding:32px 36px; position:relative; }
97
+ .content > * { position:relative; z-index:1; }
98
+
99
+ /* ── FRANKLIN watermark ── */
100
+ .watermark {
101
+ position:fixed; top:0; right:0; bottom:0; width:calc(100% - 230px);
102
+ pointer-events:none; z-index:0; overflow:hidden;
103
+ }
104
+ .watermark-text {
105
+ position:absolute; top:50%; left:50%; white-space:nowrap;
106
+ transform:translate(-50%, -50%) rotate(-25deg);
107
+ font-family:var(--sans); font-size:160px; font-weight:900;
108
+ letter-spacing:20px; text-transform:uppercase;
109
+ color:oklch(1 0 0 / 3%);
110
+ text-shadow:0 0 120px oklch(0.85 0.13 85 / 4%);
111
+ user-select:none;
112
+ }
113
+ .watermark-line2 {
114
+ position:absolute; top:calc(50% + 180px); left:50%; white-space:nowrap;
115
+ transform:translate(-50%, -50%) rotate(-25deg);
116
+ font-family:var(--mono); font-size:40px; font-weight:600;
117
+ letter-spacing:16px; text-transform:uppercase;
118
+ color:oklch(1 0 0 / 2%);
119
+ user-select:none;
120
+ }
121
+ .watermark-guilloche {
122
+ position:absolute; top:0; left:0; right:0; bottom:0;
123
+ background:
124
+ /* Top-right gold rosette */
125
+ radial-gradient(ellipse 650px 650px at 88% 6%, oklch(0.85 0.13 85 / 5%) 0%, transparent 40%),
126
+ radial-gradient(ellipse 550px 550px at 88% 6%, transparent 14%, oklch(0.85 0.13 85 / 4%) 14.8%, transparent 15.6%),
127
+ radial-gradient(ellipse 550px 550px at 88% 6%, transparent 22%, oklch(0.85 0.13 85 / 3.5%) 22.8%, transparent 23.6%),
128
+ radial-gradient(ellipse 550px 550px at 88% 6%, transparent 30%, oklch(0.85 0.13 85 / 3%) 30.8%, transparent 31.6%),
129
+ radial-gradient(ellipse 550px 550px at 88% 6%, transparent 38%, oklch(0.85 0.13 85 / 2.5%) 38.8%, transparent 39.6%),
130
+ /* Bottom-left green rosette */
131
+ radial-gradient(ellipse 500px 500px at 12% 92%, oklch(0.72 0.17 150 / 4%) 0%, transparent 35%),
132
+ radial-gradient(ellipse 400px 400px at 12% 92%, transparent 18%, oklch(0.72 0.17 150 / 3%) 18.8%, transparent 19.6%),
133
+ radial-gradient(ellipse 400px 400px at 12% 92%, transparent 30%, oklch(0.72 0.17 150 / 2.5%) 30.8%, transparent 31.6%),
134
+ /* Fine engraving lines */
135
+ repeating-linear-gradient(35deg, oklch(1 0 0 / 1.5%) 0px, oklch(1 0 0 / 1.5%) 1px, transparent 1px, transparent 5px),
136
+ repeating-linear-gradient(-55deg, oklch(1 0 0 / 1%) 0px, oklch(1 0 0 / 1%) 1px, transparent 1px, transparent 7px);
137
+ }
138
+
139
+ /* Franklin portrait — right side (same treatment as website hero) */
140
+ .watermark-portrait {
141
+ position:absolute; inset:0 0 0 auto; width:55%;
142
+ background:url(/assets/franklin-bill.jpg) top/cover no-repeat;
143
+ opacity:0.5; filter:brightness(1.4);
144
+ }
145
+ .watermark-portrait-fade {
146
+ position:absolute; inset:0 0 0 auto; width:55%;
147
+ background:linear-gradient(to right, var(--bg), transparent);
148
+ }
149
+ .watermark-portrait-bottom {
150
+ position:absolute; inset:auto 0 0 0; height:120px;
151
+ background:linear-gradient(to top, var(--bg), transparent);
48
152
  }
49
- nav button:hover { color:var(--text); }
50
- nav button.active { color:var(--accent); border-bottom-color:var(--accent); }
51
153
 
52
- main { padding:24px; max-width:1200px; margin:0 auto; }
53
- .grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:16px; }
154
+ .content-header { margin-bottom:24px; }
155
+ .content-header h2 { font-size:22px; font-weight:700; letter-spacing:-0.03em; }
156
+ .content-header p { font-size:13px; color:var(--text-dim); margin-top:4px; font-weight:400; }
157
+
158
+ .grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:12px; }
159
+ .grid-4 { grid-template-columns:repeat(4,1fr); }
54
160
  .card {
55
- background:var(--bg-card); border:1px solid var(--border);
56
- border-radius:8px; padding:16px 20px;
57
- }
58
- .card h3 { font-size:12px; color:var(--text-dim); text-transform:uppercase; letter-spacing:.5px; margin-bottom:12px; }
59
- .big { font-size:28px; font-weight:700; font-family:var(--mono); }
60
- .big.gold { color:var(--gold); }
61
- .big.green { color:var(--accent); }
62
- .sub { font-size:12px; color:var(--text-dim); margin-top:4px; }
63
-
64
- .bar-chart { display:flex; flex-direction:column; gap:6px; }
65
- .bar-row { display:flex; align-items:center; gap:8px; font-size:12px; }
66
- .bar-label { width:140px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--text-dim); font-family:var(--mono); }
67
- .bar-fill { height:16px; border-radius:3px; background:var(--accent); min-width:2px; transition:width .3s; }
68
- .bar-val { font-family:var(--mono); color:var(--text-dim); font-size:11px; }
69
-
70
- .daily-chart { display:flex; align-items:flex-end; gap:2px; height:80px; }
71
- .daily-bar { flex:1; background:var(--accent); border-radius:2px 2px 0 0; min-height:2px; transition:height .3s; opacity:.7; }
161
+ background:oklch(0.19 0.006 286 / 80%); border:1px solid var(--border);
162
+ border-radius:var(--radius); padding:20px;
163
+ backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px);
164
+ transition:border-color .15s, background .15s;
165
+ }
166
+ .card:hover { border-color:var(--border-strong); }
167
+ .card h3 {
168
+ font-size:10px; color:var(--text-dim); text-transform:uppercase;
169
+ letter-spacing:0.8px; font-weight:600; margin-bottom:12px;
170
+ }
171
+ .metric { font-size:28px; font-weight:700; font-family:var(--mono); line-height:1.1; }
172
+ .metric.brand { color:var(--brand); }
173
+ .metric.success { color:var(--success); }
174
+ .metric.gold { color:var(--gold); }
175
+ .metric.warning { color:var(--warning); }
176
+ .sub { font-size:11px; color:var(--text-dim); margin-top:6px; font-weight:400; }
177
+
178
+ /* ── Savings Hero ── */
179
+ .savings-hero {
180
+ background:linear-gradient(135deg, oklch(0.22 0.04 150 / 85%), oklch(0.19 0.006 286 / 80%) 70%);
181
+ border:1px solid oklch(0.72 0.17 150 / 12%);
182
+ border-radius:var(--radius); padding:28px; margin-bottom:12px;
183
+ display:flex; align-items:center; gap:28px;
184
+ box-shadow:0 4px 24px oklch(0 0 0 / 20%), inset 0 1px 0 oklch(1 0 0 / 4%);
185
+ backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px);
186
+ }
187
+ .savings-amount { font-size:44px; font-weight:800; font-family:var(--mono); color:var(--success); line-height:1; }
188
+ .savings-detail { flex:1; }
189
+ .savings-detail .label { font-size:10px; text-transform:uppercase; letter-spacing:0.8px; color:var(--text-muted); font-weight:600; margin-bottom:6px; }
190
+ .savings-detail .breakdown { font-size:13px; color:var(--text-muted); margin-top:10px; line-height:1.7; }
191
+ .savings-detail .breakdown span { color:var(--text); font-family:var(--mono); font-weight:600; }
192
+ .savings-pct {
193
+ font-size:56px; font-weight:900; font-family:var(--mono);
194
+ color:oklch(0.72 0.17 150 / 20%); line-height:1;
195
+ }
196
+
197
+ /* ── Bar chart ── */
198
+ .bar-chart { display:flex; flex-direction:column; gap:8px; }
199
+ .bar-row { display:flex; align-items:center; gap:10px; font-size:12px; }
200
+ .bar-label {
201
+ width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
202
+ color:var(--text-muted); font-family:var(--mono); font-size:11px; font-weight:500;
203
+ }
204
+ .bar-track { flex:1; height:6px; background:oklch(1 0 0 / 4%); border-radius:3px; overflow:hidden; }
205
+ .bar-fill {
206
+ height:100%; border-radius:3px; transition:width .5s ease;
207
+ background:linear-gradient(90deg, var(--brand), oklch(0.75 0.14 260));
208
+ }
209
+ .bar-val { font-family:var(--mono); color:var(--text-dim); font-size:10px; min-width:80px; text-align:right; }
210
+
211
+ /* ── Daily chart ── */
212
+ .daily-chart { display:flex; align-items:flex-end; gap:3px; height:100px; padding-top:8px; }
213
+ .daily-bar {
214
+ flex:1; border-radius:3px 3px 0 0; min-height:2px;
215
+ transition:height .4s ease, opacity .15s; opacity:.4; position:relative; cursor:crosshair;
216
+ background:linear-gradient(180deg, var(--brand), oklch(0.55 0.16 260));
217
+ }
72
218
  .daily-bar:hover { opacity:1; }
219
+ .daily-bar:hover::after {
220
+ content:attr(data-tip); position:absolute; bottom:calc(100% + 8px); left:50%;
221
+ transform:translateX(-50%); background:oklch(0.22 0.006 286); color:var(--text);
222
+ font-size:10px; font-family:var(--mono); padding:4px 8px; border-radius:5px;
223
+ white-space:nowrap; pointer-events:none; border:1px solid var(--border-strong);
224
+ box-shadow:0 4px 12px oklch(0 0 0 / 30%);
225
+ }
73
226
 
74
- .session-list { display:flex; flex-direction:column; gap:8px; }
227
+ /* ── Sessions ── */
228
+ .session-list { display:flex; flex-direction:column; gap:6px; }
75
229
  .session-item {
76
- background:var(--bg-card); border:1px solid var(--border); border-radius:6px;
77
- padding:12px 16px; cursor:pointer; transition:background .15s;
78
- }
79
- .session-item:hover { background:var(--bg-hover); }
80
- .session-item .meta { font-size:12px; color:var(--text-dim); font-family:var(--mono); }
81
- .session-detail { background:var(--bg-card); border:1px solid var(--border); border-radius:8px; padding:16px; margin-top:12px; }
82
- .msg { margin-bottom:12px; }
83
- .msg.user { color:var(--blue); }
84
- .msg.assistant { color:var(--text); }
85
- .msg pre { font-family:var(--mono); font-size:12px; white-space:pre-wrap; line-height:1.5; }
86
-
87
- .learning-item { padding:8px 0; border-bottom:1px solid var(--border); display:flex; gap:12px; align-items:center; }
230
+ background:oklch(0.19 0.006 286 / 75%); border:1px solid var(--border); border-radius:8px;
231
+ padding:14px 18px; cursor:pointer; transition:all .15s ease;
232
+ backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
233
+ }
234
+ .session-item:hover { background:var(--bg-card-hover); border-color:var(--border-strong); transform:translateY(-1px); }
235
+ .session-item .title { font-size:13px; font-weight:500; }
236
+ .session-item .meta { font-size:10px; color:var(--text-dim); font-family:var(--mono); margin-top:5px; font-weight:400; }
237
+ .session-detail {
238
+ background:var(--bg-card); border:1px solid var(--border); border-radius:var(--radius);
239
+ padding:20px; margin-top:14px; max-height:60vh; overflow-y:auto;
240
+ }
241
+ .msg { margin-bottom:14px; }
242
+ .msg.user .role { color:var(--brand); }
243
+ .msg.assistant .role { color:var(--success); }
244
+ .msg .role { font-size:9px; font-weight:700; text-transform:uppercase; letter-spacing:0.8px; margin-bottom:4px; }
245
+ .msg pre { font-family:var(--mono); font-size:12px; white-space:pre-wrap; line-height:1.6; color:var(--text-muted); }
246
+
247
+ /* ── Learnings ── */
248
+ .learning-item {
249
+ padding:12px 0; border-bottom:1px solid var(--border);
250
+ display:flex; gap:12px; align-items:center;
251
+ }
88
252
  .learning-item:last-child { border:none; }
89
- .confidence { font-size:11px; font-family:var(--mono); padding:2px 6px; border-radius:3px; }
90
- .confidence.high { background:#10b98133; color:var(--accent); }
91
- .confidence.mid { background:#ffd70033; color:var(--gold); }
92
- .confidence.low { background:#6a6a7a33; color:var(--text-dim); }
253
+ .badge {
254
+ font-size:9px; font-family:var(--mono); font-weight:700;
255
+ padding:3px 8px; border-radius:5px; white-space:nowrap;
256
+ }
257
+ .badge.high { background:oklch(0.72 0.17 150 / 12%); color:var(--success); }
258
+ .badge.mid { background:oklch(0.78 0.14 85 / 12%); color:var(--warning); }
259
+ .badge.low { background:oklch(1 0 0 / 5%); color:var(--text-dim); }
260
+ .learning-text { flex:1; font-size:13px; color:var(--text-muted); line-height:1.5; }
261
+ .learning-count { font-size:10px; font-family:var(--mono); color:var(--text-dim); font-weight:500; }
93
262
 
263
+ /* ── Search ── */
94
264
  .search-box {
95
- width:100%; padding:10px 16px; background:var(--bg-card); border:1px solid var(--border);
96
- border-radius:6px; color:var(--text); font-size:14px; margin-bottom:16px; outline:none;
265
+ width:100%; padding:10px 14px; background:oklch(1 0 0 / 3%); border:1px solid var(--border);
266
+ border-radius:8px; color:var(--text); font-size:13px; font-family:var(--sans);
267
+ margin-bottom:16px; outline:none; transition:border-color .2s, box-shadow .2s;
97
268
  }
98
- .search-box:focus { border-color:var(--accent); }
269
+ .search-box::placeholder { color:var(--text-dim); }
270
+ .search-box:focus { border-color:var(--brand); box-shadow:0 0 0 3px oklch(0.68 0.16 260 / 12%); }
271
+
99
272
  .tab { display:none; }
100
273
  .tab.active { display:block; }
101
- .empty { color:var(--text-dim); text-align:center; padding:40px; }
274
+ .empty { color:var(--text-dim); text-align:center; padding:56px 24px; font-size:13px; }
275
+
276
+ @media (max-width:768px) {
277
+ body { flex-direction:column; }
278
+ .sidebar { width:100%; min-width:100%; flex-direction:row; padding:8px; overflow-x:auto; border-right:none; border-bottom:1px solid var(--border); }
279
+ .sidebar-header, .sidebar-label, .sidebar-footer { display:none; }
280
+ .sidebar-nav { flex-direction:row; gap:4px; padding:0; }
281
+ .content { padding:16px; }
282
+ .grid-4 { grid-template-columns:repeat(2,1fr); }
283
+ .savings-hero { flex-direction:column; gap:12px; text-align:center; }
284
+ .savings-pct { display:none; }
285
+ .watermark { width:100%; }
286
+ }
102
287
  </style>
103
288
  </head>
104
289
  <body>
105
290
 
106
- <header>
107
- <h1><span>◆</span> Franklin Panel</h1>
108
- <div>
109
- <span id="status" style="font-size:12px;color:var(--text-dim)">connecting</span>
110
- <span class="dot off" id="dot"></span>
291
+ <!-- Sidebar -->
292
+ <aside class="sidebar">
293
+ <div class="sidebar-header">
294
+ <div class="sidebar-brand">
295
+ <div class="icon"><img src="/assets/franklin-portrait.jpg" alt="F"></div>
296
+ <h1>Franklin</h1>
297
+ </div>
298
+ <div class="sidebar-sub">by <span style="color:var(--success)">BlockRun.ai</span></div>
299
+ <div class="sidebar-status">
300
+ <span class="dot off" id="dot"></span>
301
+ <span id="status">connecting</span>
302
+ </div>
303
+ </div>
304
+
305
+ <div class="sidebar-label">Dashboard</div>
306
+ <div class="sidebar-nav">
307
+ <button class="nav-item active" data-tab="overview">
308
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></svg>
309
+ Overview
310
+ </button>
311
+ <button class="nav-item" data-tab="sessions">
312
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
313
+ Sessions
314
+ </button>
315
+ <button class="nav-item" data-tab="social">
316
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4l11.733 16h4.267l-11.733-16z"/><path d="M4 20l6.768-6.768M15.232 11.232L20 4"/></svg>
317
+ Social
318
+ </button>
319
+ <button class="nav-item" data-tab="learnings">
320
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
321
+ Learnings
322
+ </button>
111
323
  </div>
112
- </header>
113
324
 
114
- <nav>
115
- <button class="active" data-tab="overview">Overview</button>
116
- <button data-tab="sessions">Sessions</button>
117
- <button data-tab="social">Social</button>
118
- <button data-tab="learnings">Learnings</button>
119
- </nav>
325
+ <div class="sidebar-footer">
326
+ <a href="https://franklin.run" target="_blank" rel="noopener" style="display:flex;align-items:center;gap:8px;padding:8px 0 12px;color:var(--text-dim);font-size:12px;text-decoration:none;transition:color 0.15s;">
327
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
328
+ franklin.run
329
+ </a>
330
+ <div class="wallet-mini">
331
+ <span class="bal" id="sidebar-balance">&mdash;</span>
332
+ <span id="sidebar-addr">Loading wallet...</span>
333
+ </div>
334
+ </div>
335
+ </aside>
336
+
337
+ <!-- Watermark layer -->
338
+ <div class="watermark" aria-hidden="true">
339
+ <div class="watermark-guilloche"></div>
340
+ <div class="watermark-text">FRANKLIN</div>
341
+ <div class="watermark-line2">THE AI AGENT WITH A WALLET</div>
342
+ <div class="watermark-portrait"></div>
343
+ <div class="watermark-portrait-fade"></div>
344
+ <div class="watermark-portrait-bottom"></div>
345
+ </div>
120
346
 
121
- <main>
347
+ <!-- Content -->
348
+ <div class="content">
122
349
  <!-- Overview -->
123
350
  <div class="tab active" id="tab-overview">
124
- <div class="grid">
351
+ <div class="content-header">
352
+ <h2>Overview</h2>
353
+ <p>Usage stats and cost breakdown</p>
354
+ </div>
355
+
356
+ <div class="savings-hero" id="savings-hero" style="display:none">
357
+ <div>
358
+ <div class="savings-detail">
359
+ <div class="label">Saved vs Claude Opus</div>
360
+ </div>
361
+ <div class="savings-amount" id="savings-amount">&mdash;</div>
362
+ <div class="savings-detail">
363
+ <div class="breakdown">
364
+ You spent <span id="savings-actual">&mdash;</span> instead of <span id="savings-opus">&mdash;</span>
365
+ </div>
366
+ </div>
367
+ </div>
368
+ <div class="savings-pct" id="savings-pct">&mdash;</div>
369
+ </div>
370
+
371
+ <div class="grid grid-4">
125
372
  <div class="card">
126
- <h3>Wallet</h3>
127
- <div class="big gold" id="balance">—</div>
128
- <div class="sub" id="wallet-addr">Loading...</div>
373
+ <h3>Balance</h3>
374
+ <div class="metric gold" id="balance">&mdash;</div>
375
+ <div class="sub" id="wallet-chain">&mdash;</div>
129
376
  </div>
130
377
  <div class="card">
131
378
  <h3>Total Spent</h3>
132
- <div class="big green" id="total-cost">—</div>
133
- <div class="sub" id="total-requests">— requests</div>
379
+ <div class="metric brand" id="total-cost">&mdash;</div>
380
+ <div class="sub" id="total-requests">&mdash;</div>
134
381
  </div>
135
382
  <div class="card">
136
- <h3>Savings vs Opus</h3>
137
- <div class="big green" id="savings">—</div>
138
- <div class="sub">compared to Claude Opus pricing</div>
383
+ <h3>Requests</h3>
384
+ <div class="metric" id="request-count">&mdash;</div>
385
+ <div class="sub" id="avg-cost">&mdash;</div>
386
+ </div>
387
+ <div class="card">
388
+ <h3>Models Used</h3>
389
+ <div class="metric" id="model-count">&mdash;</div>
390
+ <div class="sub" id="period-info">&mdash;</div>
139
391
  </div>
140
392
  </div>
141
- <div class="card" style="margin-top:16px">
142
- <h3>Daily Cost (30 days)</h3>
393
+
394
+ <div class="card" style="margin-top:12px">
395
+ <h3>Daily Spend (30 days)</h3>
143
396
  <div class="daily-chart" id="daily-chart"></div>
144
397
  </div>
145
- <div class="card" style="margin-top:16px">
146
- <h3>Model Usage</h3>
398
+ <div class="card" style="margin-top:12px">
399
+ <h3>Cost by Model</h3>
147
400
  <div class="bar-chart" id="model-chart"></div>
148
401
  </div>
149
402
  </div>
150
403
 
151
404
  <!-- Sessions -->
152
405
  <div class="tab" id="tab-sessions">
406
+ <div class="content-header">
407
+ <h2>Sessions</h2>
408
+ <p>Browse past conversations</p>
409
+ </div>
153
410
  <input class="search-box" id="session-search" placeholder="Search sessions..." />
154
411
  <div class="session-list" id="session-list"></div>
155
412
  <div class="session-detail" id="session-detail" style="display:none"></div>
@@ -157,8 +414,12 @@ main { padding:24px; max-width:1200px; margin:0 auto; }
157
414
 
158
415
  <!-- Social -->
159
416
  <div class="tab" id="tab-social">
160
- <div class="grid" id="social-stats"></div>
161
- <div class="card" style="margin-top:16px">
417
+ <div class="content-header">
418
+ <h2>Social</h2>
419
+ <p>X/Twitter engagement stats</p>
420
+ </div>
421
+ <div class="grid grid-4" id="social-stats"></div>
422
+ <div class="card" style="margin-top:12px">
162
423
  <h3>Recent Activity</h3>
163
424
  <div id="social-feed" class="empty">No social activity yet</div>
164
425
  </div>
@@ -166,29 +427,30 @@ main { padding:24px; max-width:1200px; margin:0 auto; }
166
427
 
167
428
  <!-- Learnings -->
168
429
  <div class="tab" id="tab-learnings">
430
+ <div class="content-header">
431
+ <h2>Learnings</h2>
432
+ <p>Preferences Franklin has learned over time</p>
433
+ </div>
169
434
  <div id="learnings-list"></div>
170
435
  </div>
171
- </main>
436
+ </div>
172
437
 
173
438
  <script>
174
439
  // Tab switching
175
- document.querySelectorAll('nav button').forEach(btn => {
440
+ document.querySelectorAll('.nav-item').forEach(btn => {
176
441
  btn.addEventListener('click', () => {
177
- document.querySelectorAll('nav button').forEach(b => b.classList.remove('active'));
442
+ document.querySelectorAll('.nav-item').forEach(b => b.classList.remove('active'));
178
443
  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
179
444
  btn.classList.add('active');
180
445
  document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
181
446
  });
182
447
  });
183
448
 
184
- // API helpers
185
449
  const api = (path) => fetch('/api/' + path).then(r => r.json()).catch(() => null);
186
-
187
- // Format currency
188
450
  const usd = (n) => '$' + (n || 0).toFixed(4);
189
451
  const usdBig = (n) => '$' + (n || 0).toFixed(2);
452
+ const esc = (s) => s.replace(/</g, '&lt;').replace(/>/g, '&gt;');
190
453
 
191
- // Load overview
192
454
  async function loadOverview() {
193
455
  const [wallet, stats, insights] = await Promise.all([
194
456
  api('wallet'), api('stats'), api('insights?days=30')
@@ -196,28 +458,39 @@ async function loadOverview() {
196
458
 
197
459
  if (wallet) {
198
460
  document.getElementById('balance').textContent = usdBig(wallet.balance) + ' USDC';
199
- document.getElementById('wallet-addr').textContent = wallet.address + ' (' + wallet.chain + ')';
461
+ document.getElementById('wallet-chain').textContent = wallet.chain;
462
+ document.getElementById('sidebar-balance').textContent = usdBig(wallet.balance) + ' USDC';
463
+ const addr = wallet.address || '';
464
+ document.getElementById('sidebar-addr').textContent = addr.slice(0, 6) + '...' + addr.slice(-4);
200
465
  }
201
466
 
202
467
  if (stats) {
203
468
  document.getElementById('total-cost').textContent = usd(stats.totalCostUsd);
204
469
  document.getElementById('total-requests').textContent = stats.totalRequests.toLocaleString() + ' requests';
470
+ document.getElementById('request-count').textContent = stats.totalRequests.toLocaleString();
471
+ document.getElementById('avg-cost').textContent = usd(stats.avgCostPerRequest) + ' avg/req';
472
+ document.getElementById('model-count').textContent = Object.keys(stats.byModel || {}).length;
473
+ document.getElementById('period-info').textContent = stats.period || '';
474
+
205
475
  if (stats.opusCost > 0) {
206
- const pct = ((1 - stats.totalCostUsd / stats.opusCost) * 100).toFixed(0);
207
- document.getElementById('savings').textContent = pct + '%';
476
+ const saved = stats.saved || (stats.opusCost - stats.totalCostUsd);
477
+ const pct = stats.savedPct || ((1 - stats.totalCostUsd / stats.opusCost) * 100);
478
+ document.getElementById('savings-hero').style.display = 'flex';
479
+ document.getElementById('savings-amount').textContent = usdBig(saved);
480
+ document.getElementById('savings-pct').textContent = pct.toFixed(0) + '%';
481
+ document.getElementById('savings-actual').textContent = usd(stats.totalCostUsd);
482
+ document.getElementById('savings-opus').textContent = usdBig(stats.opusCost);
208
483
  }
209
484
 
210
- // Model chart
211
485
  const models = Object.entries(stats.byModel || {})
212
- .map(([name, d]) => ({ name, cost: d.costUsd || 0 }))
213
- .sort((a, b) => b.cost - a.cost)
214
- .slice(0, 10);
486
+ .map(([name, d]) => ({ name, cost: d.costUsd || 0, reqs: d.requests || 0 }))
487
+ .sort((a, b) => b.cost - a.cost).slice(0, 10);
215
488
  const maxCost = Math.max(...models.map(m => m.cost), 0.001);
216
489
  document.getElementById('model-chart').innerHTML = models.map(m =>
217
490
  '<div class="bar-row">' +
218
- '<span class="bar-label">' + m.name.split('/').pop() + '</span>' +
219
- '<div class="bar-fill" style="width:' + (m.cost/maxCost*100) + '%"></div>' +
220
- '<span class="bar-val">' + usd(m.cost) + '</span>' +
491
+ '<span class="bar-label">' + esc(m.name.split('/').pop()) + '</span>' +
492
+ '<div class="bar-track"><div class="bar-fill" style="width:' + (m.cost/maxCost*100) + '%"></div></div>' +
493
+ '<span class="bar-val">' + usd(m.cost) + ' (' + m.reqs + ')</span>' +
221
494
  '</div>'
222
495
  ).join('');
223
496
  }
@@ -226,12 +499,11 @@ async function loadOverview() {
226
499
  const days = insights.dailyCosts.slice(-30);
227
500
  const maxDay = Math.max(...days.map(d => d.cost), 0.001);
228
501
  document.getElementById('daily-chart').innerHTML = days.map(d =>
229
- '<div class="daily-bar" title="' + d.date + ': ' + usd(d.cost) + '" style="height:' + (d.cost/maxDay*100) + '%"></div>'
502
+ '<div class="daily-bar" data-tip="' + d.date + ': ' + usd(d.cost) + '" style="height:' + Math.max(d.cost/maxDay*100, 2) + '%"></div>'
230
503
  ).join('');
231
504
  }
232
505
  }
233
506
 
234
- // Load sessions
235
507
  async function loadSessions() {
236
508
  const sessions = await api('sessions');
237
509
  if (!sessions || sessions.length === 0) {
@@ -239,30 +511,26 @@ async function loadSessions() {
239
511
  return;
240
512
  }
241
513
  document.getElementById('session-list').innerHTML = sessions.slice(0, 50).map(s =>
242
- '<div class="session-item" data-id="' + s.id + '">' +
243
- '<div>' + (s.model || 'unknown') + ' ' + s.messageCount + ' messages</div>' +
244
- '<div class="meta">' + new Date(s.createdAt).toLocaleString() + ' · ' + (s.workDir || '').split('/').pop() + '</div>' +
514
+ '<div class="session-item" data-id="' + esc(s.id) + '">' +
515
+ '<div class="title">' + esc(s.model || 'unknown') + ' &mdash; ' + s.messageCount + ' messages</div>' +
516
+ '<div class="meta">' + new Date(s.createdAt).toLocaleString() + ' &middot; ' + esc((s.workDir || '').split('/').pop()) + '</div>' +
245
517
  '</div>'
246
518
  ).join('');
247
-
248
519
  document.querySelectorAll('.session-item').forEach(el => {
249
520
  el.addEventListener('click', async () => {
250
- const id = el.dataset.id;
251
- const history = await api('sessions/' + encodeURIComponent(id));
521
+ const history = await api('sessions/' + encodeURIComponent(el.dataset.id));
252
522
  if (!history) return;
253
523
  const detail = document.getElementById('session-detail');
254
524
  detail.style.display = 'block';
255
525
  detail.innerHTML = history.map(m => {
256
526
  const role = m.role || 'system';
257
527
  let text = typeof m.content === 'string' ? m.content : JSON.stringify(m.content).slice(0, 500);
258
- text = text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
259
- return '<div class="msg ' + role + '"><pre>' + role.toUpperCase() + ': ' + text + '</pre></div>';
528
+ return '<div class="msg ' + role + '"><div class="role">' + role + '</div><pre>' + esc(text) + '</pre></div>';
260
529
  }).join('');
261
530
  });
262
531
  });
263
532
  }
264
533
 
265
- // Session search
266
534
  let searchTimeout;
267
535
  document.getElementById('session-search').addEventListener('input', (e) => {
268
536
  clearTimeout(searchTimeout);
@@ -276,25 +544,23 @@ document.getElementById('session-search').addEventListener('input', (e) => {
276
544
  }
277
545
  document.getElementById('session-list').innerHTML = results.map(r =>
278
546
  '<div class="session-item">' +
279
- '<div>' + r.snippet.replace(/</g, '&lt;') + '</div>' +
280
- '<div class="meta">' + r.sessionId + ' · score: ' + r.score.toFixed(2) + '</div>' +
547
+ '<div class="title">' + esc(r.snippet) + '</div>' +
548
+ '<div class="meta">' + esc(r.sessionId) + ' &middot; score: ' + r.score.toFixed(2) + '</div>' +
281
549
  '</div>'
282
550
  ).join('');
283
551
  }, 300);
284
552
  });
285
553
 
286
- // Load social
287
554
  async function loadSocial() {
288
555
  const social = await api('social');
289
- if (!social) { return; }
556
+ if (!social) return;
290
557
  document.getElementById('social-stats').innerHTML =
291
- '<div class="card"><h3>Posted</h3><div class="big green">' + (social.posted || 0) + '</div></div>' +
292
- '<div class="card"><h3>Drafted</h3><div class="big">' + (social.drafted || 0) + '</div></div>' +
293
- '<div class="card"><h3>Skipped</h3><div class="big">' + (social.skipped || 0) + '</div></div>' +
294
- '<div class="card"><h3>Total Cost</h3><div class="big gold">' + usd(social.totalCost || 0) + '</div></div>';
558
+ '<div class="card"><h3>Posted</h3><div class="metric success">' + (social.posted || 0) + '</div></div>' +
559
+ '<div class="card"><h3>Drafted</h3><div class="metric">' + (social.drafted || 0) + '</div></div>' +
560
+ '<div class="card"><h3>Skipped</h3><div class="metric">' + (social.skipped || 0) + '</div></div>' +
561
+ '<div class="card"><h3>Social Cost</h3><div class="metric gold">' + usd(social.totalCost || 0) + '</div></div>';
295
562
  }
296
563
 
297
- // Load learnings
298
564
  async function loadLearnings() {
299
565
  const learnings = await api('learnings');
300
566
  if (!learnings || learnings.length === 0) {
@@ -306,34 +572,31 @@ async function loadLearnings() {
306
572
  .map(l => {
307
573
  const cls = l.confidence >= 0.8 ? 'high' : l.confidence >= 0.5 ? 'mid' : 'low';
308
574
  return '<div class="learning-item">' +
309
- '<span class="confidence ' + cls + '">' + (l.confidence * 100).toFixed(0) + '%</span>' +
310
- '<span>' + l.learning + '</span>' +
311
- '<span style="margin-left:auto;color:var(--text-dim);font-size:11px">×' + l.times_confirmed + '</span>' +
575
+ '<span class="badge ' + cls + '">' + (l.confidence * 100).toFixed(0) + '%</span>' +
576
+ '<span class="learning-text">' + esc(l.learning) + '</span>' +
577
+ '<span class="learning-count">&times;' + l.times_confirmed + '</span>' +
312
578
  '</div>';
313
579
  }).join('');
314
580
  }
315
581
 
316
- // SSE
317
582
  const es = new EventSource('/api/events');
318
583
  const dot = document.getElementById('dot');
319
584
  const statusEl = document.getElementById('status');
320
585
  es.onopen = () => { dot.className = 'dot on'; statusEl.textContent = 'live'; };
321
- es.onerror = () => { dot.className = 'dot off'; statusEl.textContent = 'disconnected'; };
586
+ es.onerror = () => { dot.className = 'dot off'; statusEl.textContent = 'offline'; };
322
587
  es.onmessage = (e) => {
323
- try {
324
- const evt = JSON.parse(e.data);
325
- if (evt.type === 'stats.updated') loadOverview();
326
- } catch {}
588
+ try { if (JSON.parse(e.data).type === 'stats.updated') loadOverview(); } catch {}
327
589
  };
328
590
 
329
- // Init
330
591
  loadOverview();
331
592
  loadSessions();
332
593
  loadSocial();
333
594
  loadLearnings();
334
- // Refresh wallet balance every 30s
335
595
  setInterval(() => api('wallet').then(w => {
336
- if (w) document.getElementById('balance').textContent = usdBig(w.balance) + ' USDC';
596
+ if (w) {
597
+ document.getElementById('balance').textContent = usdBig(w.balance) + ' USDC';
598
+ document.getElementById('sidebar-balance').textContent = usdBig(w.balance) + ' USDC';
599
+ }
337
600
  }), 30000);
338
601
  </script>
339
602
  </body>