@enigmax/dashboard 0.1.0 → 0.1.2

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 (3) hide show
  1. package/README.md +8 -14
  2. package/assets/index.html +273 -102
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,22 +1,16 @@
1
1
  # @enigmax/dashboard
2
2
 
3
3
  The browser UI for [enigma](https://github.com/FJRG2007/enigma)'s local savings
4
- dashboard: a single static page (`assets/index.html`) plus a vendored charting library
5
- (`assets/lib/chart.min.js`).
4
+ dashboard: a single static page plus a vendored charting library.
6
5
 
7
- You do not install this directly. enigma-cli fetches it **on demand** the first time you
8
- open the dashboard (`enigma dashboard`) or enable it (`enigma config dashboard on`), into
9
- a managed directory under `~/.enigma/dashboard`, and keeps it up to date on
10
- `enigma update`. enigma's loopback HTTP server serves these files and provides the
11
- `/api/stats` and `/api/settings` endpoints the page talks to.
6
+ You do not install this directly. enigma fetches it on demand the first time you open the
7
+ dashboard (`enigma dashboard`) or enable it (`enigma config dashboard on`), and keeps it up
8
+ to date on `enigma update`.
12
9
 
13
- Keeping the UI out of the base `enigma-cli` package means users who never open the
14
- dashboard never download it (the chart library alone is ~196 KB).
15
-
16
- The page is served only on `127.0.0.1` (never network-facing) and the settings write
17
- endpoint is origin-guarded by enigma. The chart library retains its upstream Apache-2.0
18
- license header as required; its attribution logo is suppressed in enigma's own CSS.
10
+ Shipping the UI separately keeps it out of the base `enigma-cli` package, so people who
11
+ never open the dashboard never download it. The dashboard runs only on your own machine
12
+ (`127.0.0.1`); it is never exposed to the network.
19
13
 
20
14
  ## License
21
15
 
22
- Apache-2.0
16
+ Apache-2.0. The bundled charting library retains its upstream Apache-2.0 license header.
package/assets/index.html CHANGED
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Enigma - Context Savings</title>
6
+ <title>Enigma - Dashboard</title>
7
7
  <style>
8
8
  :root {
9
9
  --bg: #0b0e14; --surface: #151a23; --surface2: #1b2230; --border: #232a36;
@@ -13,13 +13,21 @@
13
13
  body {
14
14
  margin: 0; background: var(--bg); color: var(--text);
15
15
  font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
16
- -webkit-font-smoothing: antialiased; padding: 24px; max-width: 1000px; margin: 0 auto;
16
+ -webkit-font-smoothing: antialiased; padding: 24px; max-width: 1040px; margin: 0 auto;
17
17
  }
18
18
  a { color: var(--accent2); }
19
- header { display: flex; align-items: center; gap: 12px; margin-bottom: 24px; }
19
+ header { display: flex; align-items: center; gap: 20px; margin-bottom: 24px; flex-wrap: wrap; }
20
+ .brand { display: flex; flex-direction: column; }
20
21
  .wordmark { font-size: 20px; font-weight: 700; letter-spacing: 0.5px; }
21
22
  .wordmark span { color: var(--accent); }
22
23
  .sub { color: var(--muted); font-size: 13px; }
24
+ .tabs { display: flex; gap: 4px; }
25
+ .tabs .tab {
26
+ text-decoration: none; color: var(--muted); font-size: 13px; font-weight: 600;
27
+ padding: 7px 16px; border-radius: 8px; border: 1px solid transparent; transition: background .15s, color .15s, border-color .15s;
28
+ }
29
+ .tabs .tab:hover { color: var(--text); background: var(--surface); }
30
+ .tabs .tab.active { color: var(--accent); background: var(--surface); border-color: var(--border); }
23
31
  .live { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 12px; }
24
32
  .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--good); box-shadow: 0 0 8px var(--good); }
25
33
  .dot.stale { background: var(--muted); box-shadow: none; }
@@ -76,6 +84,9 @@
76
84
  .stat-line b { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
77
85
  footer { color: var(--muted); font-size: 12px; line-height: 1.6; margin-top: 8px; }
78
86
  code { background: var(--surface2); padding: 1px 6px; border-radius: 5px; color: var(--accent2); }
87
+ .page-head { margin-bottom: 18px; }
88
+ .page-head h1 { font-size: 18px; font-weight: 700; margin: 0 0 4px; }
89
+ .page-head p { color: var(--muted); font-size: 13px; margin: 0; max-width: 720px; line-height: 1.5; }
79
90
  .set-cat { margin-bottom: 18px; }
80
91
  .set-cat:last-child { margin-bottom: 0; }
81
92
  .set-cat-h { color: var(--accent); font-size: 12px; text-transform: uppercase; letter-spacing: 0.6px; margin-bottom: 4px; }
@@ -97,15 +108,36 @@
97
108
  border-radius: 7px; padding: 5px 10px; font-size: 12px; font-family: inherit; cursor: pointer;
98
109
  }
99
110
  .set-note { color: var(--accent); font-size: 12px; min-height: 16px; margin-top: 10px; }
100
- @media (max-width: 720px) { .grid { grid-template-columns: repeat(2, 1fr); } }
111
+ .tag {
112
+ display: inline-block; font-size: 11px; color: var(--muted); background: var(--surface2);
113
+ border: 1px solid var(--border); border-radius: 6px; padding: 1px 7px; margin-left: 6px; vertical-align: middle;
114
+ }
115
+ .tag.ext { color: var(--accent2); }
116
+ .tag.warn { color: var(--accent); border-color: var(--accent); }
117
+ .editor {
118
+ width: 100%; min-height: 360px; resize: vertical; background: var(--surface2); color: var(--text);
119
+ border: 1px solid var(--border); border-radius: 8px; padding: 12px;
120
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.5;
121
+ }
122
+ .btn-danger {
123
+ background: var(--surface2); color: var(--accent2); border: 1px solid var(--border);
124
+ border-radius: 7px; padding: 5px 14px; font-size: 12px; cursor: pointer; font-family: inherit;
125
+ }
126
+ .btn-danger:hover { border-color: var(--accent2); }
127
+ @media (max-width: 720px) { .grid { grid-template-columns: repeat(2, 1fr); } header { gap: 12px; } }
101
128
  </style>
102
129
  </head>
103
130
  <body>
104
131
  <header>
105
- <div>
132
+ <div class="brand">
106
133
  <div class="wordmark">Enigma<span>.</span></div>
107
- <div class="sub">Context Compression Savings</div>
134
+ <div class="sub">Savings dashboard</div>
108
135
  </div>
136
+ <nav class="tabs">
137
+ <a href="#/" data-view="savings" class="tab active">Savings</a>
138
+ <a href="#/skills" data-view="skills" class="tab">Skills</a>
139
+ <a href="#/settings" data-view="settings" class="tab">Settings</a>
140
+ </nav>
109
141
  <div class="nav">
110
142
  <span class="live"><span id="dot" class="dot stale"></span><span id="updated">Connecting...</span></span>
111
143
  <nav class="links">
@@ -116,112 +148,140 @@
116
148
  <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.317 4.3698a19.7913 19.7913 0 0 0-4.8851-1.5152.0741.0741 0 0 0-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 0 0-.0785-.037 19.7363 19.7363 0 0 0-4.8852 1.515.0699.0699 0 0 0-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 0 0 .0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 0 0 .0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 0 0-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 0 1-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 0 1 .0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 0 1 .0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 0 1-.0066.1276 12.2986 12.2986 0 0 1-1.873.8914.0766.0766 0 0 0-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 0 0 .0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 0 0 .0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 0 0-.0312-.0286ZM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189Zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9461 2.4189-2.1568 2.4189Z"/></svg>
117
149
  </a>
118
150
  <a href="https://ko-fi.com/fjrg2007" target="_blank" rel="noopener noreferrer" title="Support on Ko-fi" aria-label="Support on Ko-fi">
119
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 8h1a4 4 0 1 1 0 8h-1"/><path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"/><line x1="6" y1="2" x2="6" y2="4"/><line x1="10" y1="2" x2="10" y2="4"/><line x1="14" y1="2" x2="14" y2="4"/></svg>
151
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 8h1a4 4 0 1 1 0 8h-1"/><path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"/><line x1="6" y1="2" x2="6" y2="4"/><line x1="14" y1="2" x2="14" y2="4"/></svg>
120
152
  </a>
121
153
  </nav>
122
154
  </div>
123
155
  </header>
124
156
 
125
- <div class="grid">
126
- <div class="card"><div class="label">Money Saved</div><div id="money" class="value good">-</div><div id="moneyFoot" class="foot">Estimated, by source</div></div>
127
- <div class="card"><div class="label">Time Saved</div><div id="time" class="value accent">-</div><div id="timeFoot" class="foot">Estimated prefill time</div></div>
128
- <div class="card"><div class="label">Tokens Saved</div><div id="saved" class="value">-</div><div id="savedFoot" class="foot">Across all compressions</div></div>
129
- <div class="card"><div class="label">Savings Rate</div><div id="rate" class="value accent">-</div><div class="foot">Of input tokens removed</div></div>
130
- <div class="card"><div class="label">Compressions</div><div id="calls" class="value">-</div><div class="foot">Reversible via CCR</div></div>
131
- <div class="card"><div class="label">Avg / Compression</div><div id="avg" class="value">-</div><div class="foot">Tokens saved per call</div></div>
132
- <div class="card"><div class="label">Best Compression</div><div id="best" class="value">-</div><div class="foot">Most saved in one call</div></div>
133
- </div>
134
-
135
- <div class="panel">
136
- <div class="panel-head">
137
- <h2>Savings Per Day <small id="rangeLabel"></small></h2>
138
- <div class="ctrls">
139
- <div class="ranges" id="metricToggle">
140
- <button type="button" data-m="tokens" class="active">Tokens</button>
141
- <button type="button" data-m="usd">$</button>
142
- </div>
143
- <div class="ranges" id="modeToggle">
144
- <button type="button" data-o="daily" class="active">Daily</button>
145
- <button type="button" data-o="cumulative">Cumulative</button>
146
- </div>
147
- <div class="ranges" id="ranges">
148
- <button type="button" data-d="7">7D</button>
149
- <button type="button" data-d="30" class="active">30D</button>
150
- <button type="button" data-d="90">90D</button>
151
- <button type="button" data-d="0">All</button>
157
+ <main id="view-savings">
158
+ <div class="grid">
159
+ <div class="card"><div class="label">Money Saved</div><div id="money" class="value good">-</div><div id="moneyFoot" class="foot">Estimated, by source</div></div>
160
+ <div class="card"><div class="label">Time Saved</div><div id="time" class="value accent">-</div><div id="timeFoot" class="foot">Estimated prefill time</div></div>
161
+ <div class="card"><div class="label">Tokens Saved</div><div id="saved" class="value">-</div><div id="savedFoot" class="foot">Across all compressions</div></div>
162
+ <div class="card"><div class="label">Savings Rate</div><div id="rate" class="value accent">-</div><div class="foot">Of input tokens removed</div></div>
163
+ <div class="card"><div class="label">Compressions</div><div id="calls" class="value">-</div><div class="foot">Reversible via CCR</div></div>
164
+ <div class="card"><div class="label">Avg / Compression</div><div id="avg" class="value">-</div><div class="foot">Tokens saved per call</div></div>
165
+ <div class="card"><div class="label">Best Compression</div><div id="best" class="value">-</div><div class="foot">Most saved in one call</div></div>
166
+ </div>
167
+
168
+ <div class="panel">
169
+ <div class="panel-head">
170
+ <h2>Savings Per Day <small id="rangeLabel"></small></h2>
171
+ <div class="ctrls">
172
+ <div class="ranges" id="metricToggle">
173
+ <button type="button" data-m="tokens" class="active">Tokens</button>
174
+ <button type="button" data-m="usd">$</button>
175
+ </div>
176
+ <div class="ranges" id="modeToggle">
177
+ <button type="button" data-o="daily" class="active">Daily</button>
178
+ <button type="button" data-o="cumulative">Cumulative</button>
179
+ </div>
180
+ <div class="ranges" id="ranges">
181
+ <button type="button" data-d="7">7D</button>
182
+ <button type="button" data-d="30" class="active">30D</button>
183
+ <button type="button" data-d="90">90D</button>
184
+ <button type="button" data-d="0">All</button>
185
+ </div>
152
186
  </div>
153
187
  </div>
188
+ <div id="chartWrap" class="chartwrap"></div>
189
+ <div id="chartEmpty" class="empty" style="display:none">No compression activity recorded yet. Run <code>enigma compress &lt;file&gt;</code> or enable the compression MCP (<code>enigma config compress on</code>).</div>
190
+ </div>
191
+
192
+ <div class="panel" id="usagePanel" style="display:none">
193
+ <h2 style="margin-bottom:4px">Real Tool Usage <small id="usageSub">Claude Code transcripts</small></h2>
194
+ <div class="sub" style="margin-bottom:14px">Measured token consumption and real prompt-cache savings, read-only from your own session logs. Deliberately not attributed to skills or output-style: a transcript has no counterfactual baseline, so any such number would be invented.</div>
195
+ <div class="grid" style="margin-bottom:16px">
196
+ <div class="card"><div class="label">Cache Saved</div><div id="uCacheMoney" class="value good">-</div><div class="foot">Est. from prompt caching</div></div>
197
+ <div class="card"><div class="label">Cache Reads</div><div id="uCacheRead" class="value accent">-</div><div class="foot">Tokens served from cache</div></div>
198
+ <div class="card"><div class="label">Input Tokens</div><div id="uInput" class="value">-</div><div class="foot">Consumed across sessions</div></div>
199
+ <div class="card"><div class="label">Output Tokens</div><div id="uOutput" class="value">-</div><div class="foot">Generated by the agent</div></div>
200
+ <div class="card"><div class="label">Sessions</div><div id="uSessions" class="value">-</div><div id="uSessionsFoot" class="foot">Transcripts scanned</div></div>
201
+ </div>
202
+ <h2 style="margin:0 0 10px">By Model</h2>
203
+ <div id="uByModel"></div>
204
+ </div>
205
+
206
+ <div class="panel" id="usageHint" style="display:none">
207
+ <h2 style="margin-bottom:8px">Real Tool Usage <small>opt-in</small></h2>
208
+ <div class="sub">Show real token consumption and prompt-cache savings from your Claude Code sessions in <a href="#/settings">Settings</a> (Real tool-usage stats). enigma reads only your local session transcripts; nothing is sent anywhere.</div>
209
+ </div>
210
+
211
+ <div class="panel">
212
+ <h2 style="margin-bottom:14px">Savings By Source <small>which app/CLI compressed</small></h2>
213
+ <div id="sources"></div>
214
+ </div>
215
+
216
+ <div class="panel">
217
+ <h2 style="margin-bottom:14px">Savings By Content Type <small>what enigma compressed</small></h2>
218
+ <div id="types"></div>
154
219
  </div>
155
- <div id="chartWrap" class="chartwrap"></div>
156
- <div id="chartEmpty" class="empty" style="display:none">No compression activity recorded yet. Run <code>enigma compress &lt;file&gt;</code> or enable the compression MCP (<code>enigma config compress on</code>).</div>
157
- </div>
158
-
159
- <div class="panel" id="usagePanel" style="display:none">
160
- <h2 style="margin-bottom:4px">Real Tool Usage <small id="usageSub">Claude Code transcripts</small></h2>
161
- <div class="sub" style="margin-bottom:14px">Measured token consumption and real prompt-cache savings, read-only from your own session logs. Deliberately not attributed to skills or output-style: a transcript has no counterfactual baseline, so any such number would be invented.</div>
162
- <div class="grid" style="margin-bottom:16px">
163
- <div class="card"><div class="label">Cache Saved</div><div id="uCacheMoney" class="value good">-</div><div class="foot">Est. from prompt caching</div></div>
164
- <div class="card"><div class="label">Cache Reads</div><div id="uCacheRead" class="value accent">-</div><div class="foot">Tokens served from cache</div></div>
165
- <div class="card"><div class="label">Input Tokens</div><div id="uInput" class="value">-</div><div class="foot">Consumed across sessions</div></div>
166
- <div class="card"><div class="label">Output Tokens</div><div id="uOutput" class="value">-</div><div class="foot">Generated by the agent</div></div>
167
- <div class="card"><div class="label">Sessions</div><div id="uSessions" class="value">-</div><div id="uSessionsFoot" class="foot">Transcripts scanned</div></div>
220
+
221
+ <div class="panel">
222
+ <h2 style="margin-bottom:14px">Before vs. After <small id="ratioLabel">cumulative tokens</small></h2>
223
+ <div class="bar-row"><span class="name">Before</span><div class="bar-track"><div id="barBefore" class="bar-fill" style="background:var(--accent2)"></div></div><span id="numBefore" class="num">-</span></div>
224
+ <div class="bar-row"><span class="name">After</span><div class="bar-track"><div id="barAfter" class="bar-fill" style="background:var(--good)"></div></div><span id="numAfter" class="num">-</span></div>
225
+ <div class="stat-line" style="margin:14px 0 0"><div>Removed <b id="removed">-</b></div><div>Reduction <b id="reduction">-</b></div><div>Compression ratio <b id="ratio">-</b></div></div>
168
226
  </div>
169
- <h2 style="margin:0 0 10px">By Model</h2>
170
- <div id="uByModel"></div>
171
- </div>
172
-
173
- <div class="panel" id="usageHint" style="display:none">
174
- <h2 style="margin-bottom:8px">Real Tool Usage <small>opt-in</small></h2>
175
- <div class="sub">Show real token consumption and prompt-cache savings from your Claude Code sessions with <code>enigma config usage-stats on</code>. enigma reads only your local session transcripts; nothing is sent anywhere.</div>
176
- </div>
177
-
178
- <div class="panel">
179
- <h2 style="margin-bottom:14px">Savings By Source <small>which app/CLI compressed</small></h2>
180
- <div id="sources"></div>
181
- </div>
182
-
183
- <div class="panel">
184
- <h2 style="margin-bottom:14px">Savings By Content Type <small>what enigma compressed</small></h2>
185
- <div id="types"></div>
186
- </div>
187
-
188
- <div class="panel">
189
- <h2 style="margin-bottom:14px">Before vs. After <small id="ratioLabel">cumulative tokens</small></h2>
190
- <div class="bar-row"><span class="name">Before</span><div class="bar-track"><div id="barBefore" class="bar-fill" style="background:var(--accent2)"></div></div><span id="numBefore" class="num">-</span></div>
191
- <div class="bar-row"><span class="name">After</span><div class="bar-track"><div id="barAfter" class="bar-fill" style="background:var(--good)"></div></div><span id="numAfter" class="num">-</span></div>
192
- <div class="stat-line" style="margin:14px 0 0"><div>Removed <b id="removed">-</b></div><div>Reduction <b id="reduction">-</b></div><div>Compression ratio <b id="ratio">-</b></div></div>
193
- </div>
194
-
195
- <div class="panel">
196
- <div class="panel-head">
197
- <h2>Savings History <small id="histStats"></small></h2>
198
- <div class="ctrls">
199
- <div class="ranges" id="histToggle">
200
- <button type="button" data-p="day" class="active">Daily</button>
201
- <button type="button" data-p="week">Weekly</button>
202
- <button type="button" data-p="month">Monthly</button>
227
+
228
+ <div class="panel">
229
+ <div class="panel-head">
230
+ <h2>Savings History <small id="histStats"></small></h2>
231
+ <div class="ctrls">
232
+ <div class="ranges" id="histToggle">
233
+ <button type="button" data-p="day" class="active">Daily</button>
234
+ <button type="button" data-p="week">Weekly</button>
235
+ <button type="button" data-p="month">Monthly</button>
236
+ </div>
203
237
  </div>
204
238
  </div>
239
+ <div class="tbl-wrap"><div id="histBody"></div></div>
240
+ </div>
241
+
242
+ <div class="panel">
243
+ <h2 style="margin-bottom:14px">Recent Compressions <small>newest first</small></h2>
244
+ <div class="tbl-wrap"><div id="recent"></div></div>
245
+ </div>
246
+
247
+ <div class="panel">
248
+ <h2 style="margin-bottom:14px">Reversible Cache (CCR) <small>recoverable originals on disk</small></h2>
249
+ <div id="cache"></div>
250
+ </div>
251
+ </main>
252
+
253
+ <section id="view-skills" style="display:none">
254
+ <div class="page-head" style="display:flex;align-items:flex-start;gap:16px">
255
+ <div style="flex:1">
256
+ <h1>Skills</h1>
257
+ <p>The skills installed in your agents - enigma's own and any external ones you added. Edit a skill's content, disable/enable or remove it, and check whether enigma's skills are up to date.</p>
258
+ </div>
259
+ <button id="skillsCheck" type="button" class="toggle on" style="white-space:nowrap">Check for updates</button>
260
+ </div>
261
+ <div id="skillEditor" class="panel" style="display:none">
262
+ <div class="panel-head"><h2>Editing <span id="seName"></span> <small id="seWarn"></small></h2></div>
263
+ <textarea id="seText" class="editor" spellcheck="false"></textarea>
264
+ <div style="margin-top:10px;display:flex;gap:8px">
265
+ <button id="seSave" type="button" class="toggle on">Save</button>
266
+ <button id="seCancel" type="button" class="toggle">Cancel</button>
267
+ </div>
205
268
  </div>
206
- <div class="tbl-wrap"><div id="histBody"></div></div>
207
- </div>
208
-
209
- <div class="panel">
210
- <h2 style="margin-bottom:14px">Recent Compressions <small>newest first</small></h2>
211
- <div class="tbl-wrap"><div id="recent"></div></div>
212
- </div>
213
-
214
- <div class="panel">
215
- <h2 style="margin-bottom:14px">Reversible Cache (CCR) <small>recoverable originals on disk</small></h2>
216
- <div id="cache"></div>
217
- </div>
218
-
219
- <div class="panel">
220
- <h2 style="margin-bottom:4px">Enigma Settings <small>same options as the terminal UI</small></h2>
221
- <div class="sub" style="margin-bottom:14px">Everything configurable from <code>enigma config</code> / the TUI, editable here. Changes apply at global scope and take effect immediately; memory toggles need an agent restart. Numeric estimates use <code>enigma config token-price &lt;usd&gt;</code> and <code>enigma config token-speed &lt;tok/s&gt;</code>.</div>
222
- <div id="settingsBody"><div class="empty" style="padding:24px 0">Loading settings...</div></div>
223
- <div id="settingsNote" class="set-note"></div>
224
- </div>
269
+ <div class="panel">
270
+ <div id="skillsBody"><div class="empty" style="padding:24px 0">Loading skills...</div></div>
271
+ <div id="skillsNote" class="set-note"></div>
272
+ </div>
273
+ </section>
274
+
275
+ <section id="view-settings" style="display:none">
276
+ <div class="page-head">
277
+ <h1>Settings</h1>
278
+ <p>Everything you can configure with <code>enigma config</code> or the terminal UI, editable here. Changes apply at global scope and take effect immediately; toggles that change agent memory need an agent restart.</p>
279
+ </div>
280
+ <div class="panel">
281
+ <div id="settingsBody"><div class="empty" style="padding:24px 0">Loading settings...</div></div>
282
+ <div id="settingsNote" class="set-note"></div>
283
+ </div>
284
+ </section>
225
285
 
226
286
  <footer>
227
287
  Polling pauses automatically when this tab is hidden, and the server caches each snapshot - so an open dashboard costs almost nothing.
@@ -579,7 +639,7 @@
579
639
  }
580
640
  }
581
641
 
582
- // --- settings panel (mirrors the TUI registry over /api/settings) ---
642
+ // --- settings subpage (mirrors the TUI registry over /api/settings) ---
583
643
  function esc(s) { return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c])); }
584
644
 
585
645
  function settingRow(s) {
@@ -627,6 +687,7 @@
627
687
  });
628
688
  }
629
689
 
690
+ let settingsLoaded = false;
630
691
  async function loadSettings() {
631
692
  try {
632
693
  const res = await fetch("/api/settings", { cache: "no-store" });
@@ -634,12 +695,122 @@
634
695
  } catch { $("settingsBody").innerHTML = '<div class="empty" style="padding:24px 0">Settings unavailable.</div>'; }
635
696
  }
636
697
 
698
+ // --- skills subpage (lists enigma + external skills over /api/skills) ---
699
+ function skillRow(s) {
700
+ const ver = s.version ? '<span class="tag">v' + esc(s.version) + "</span>" : "";
701
+ const where = s.agents && s.agents.length ? '<span class="tag">' + s.agents.map(esc).join(", ") + "</span>"
702
+ : (s.discarded ? '<span class="tag">disabled</span>' : '<span class="tag">not deployed</span>');
703
+ const ext = s.source === "external" ? '<span class="tag ext">external</span>' : "";
704
+ const upd = s.source !== "enigma" ? ""
705
+ : s.update === "update" ? '<span class="tag warn">update available</span>'
706
+ : s.update === "modified" ? '<span class="tag ext">modified</span>' : "";
707
+ const edit = '<button type="button" class="toggle" data-name="' + esc(s.name) + '" data-action="edit" data-enigma="' + (s.source === "enigma" ? 1 : 0) + '">Edit</button>';
708
+ const main = s.source === "enigma"
709
+ ? '<button type="button" class="toggle' + (s.discarded ? "" : " on") + '" data-name="' + esc(s.name)
710
+ + '" data-action="' + (s.discarded ? "enable" : "disable") + '">' + (s.discarded ? "Disabled" : "Enabled") + "</button>"
711
+ : '<button type="button" class="btn-danger" data-name="' + esc(s.name) + '" data-action="remove">Remove</button>';
712
+ return '<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(s.name) + ext + ver + upd + where + "</div>"
713
+ + '<div class="set-hint">' + esc(s.description || "") + '</div></div><div class="set-ctl" style="display:flex;gap:8px">' + edit + main + "</div></div>";
714
+ }
715
+
716
+ function renderSkills(skills) {
717
+ const enigma = skills.filter((s) => s.source === "enigma");
718
+ const ext = skills.filter((s) => s.source === "external");
719
+ const section = (title, blurb, list, emptyText) => '<div class="set-cat"><div class="set-cat-h">' + title + "</div>"
720
+ + '<div class="set-cat-b">' + blurb + "</div>"
721
+ + (list.length ? list.map(skillRow).join("") : '<div class="empty" style="padding:16px 0">' + emptyText + "</div>") + "</div>";
722
+ $("skillsBody").innerHTML =
723
+ section("Enigma skills", "Distributed and kept current by enigma. Disable to remove from your agents; enable to restore.", enigma, "No enigma skills.")
724
+ + section("External skills", "Skills enigma did not author, found in your agents. Remove deletes the skill from every agent that has it.", ext, "No external skills found.");
725
+ }
726
+
727
+ async function postSkill(name, action) {
728
+ $("skillsNote").textContent = "Working...";
729
+ try {
730
+ const res = await fetch("/api/skills", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, action }) });
731
+ const out = await res.json();
732
+ if (!out.ok) { $("skillsNote").textContent = "Could not " + action + " " + name + ": " + (out.error || "error"); return; }
733
+ if (out.skills) renderSkills(out.skills);
734
+ $("skillsNote").textContent = out.note || ("Done: " + name);
735
+ } catch { $("skillsNote").textContent = "Could not reach the server to change " + name + "."; }
736
+ }
737
+
738
+ // Inline SKILL.md editor (one skill at a time).
739
+ let editing = null;
740
+ async function openEditor(name, isEnigma) {
741
+ $("skillsNote").textContent = "Loading " + name + "...";
742
+ try {
743
+ const res = await fetch("/api/skills", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, action: "read" }) });
744
+ const out = await res.json();
745
+ if (!out.ok) { $("skillsNote").textContent = "Could not open " + name + ": " + (out.error || "error"); return; }
746
+ editing = name;
747
+ $("seName").textContent = name;
748
+ $("seWarn").textContent = isEnigma ? "managed by enigma - local edits revert on the next update/sync" : "external skill";
749
+ $("seText").value = out.content || "";
750
+ $("skillEditor").style.display = "";
751
+ $("skillsNote").textContent = "";
752
+ window.scrollTo({ top: 0, behavior: "smooth" });
753
+ $("seText").focus();
754
+ } catch { $("skillsNote").textContent = "Could not reach the server."; }
755
+ }
756
+ function closeEditor() { editing = null; $("skillEditor").style.display = "none"; }
757
+ async function saveEditor() {
758
+ if (!editing) return;
759
+ $("skillsNote").textContent = "Saving " + editing + "...";
760
+ try {
761
+ const res = await fetch("/api/skills", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: editing, action: "save", content: $("seText").value }) });
762
+ const out = await res.json();
763
+ if (!out.ok) { $("skillsNote").textContent = "Could not save " + editing + ": " + (out.error || "error"); return; }
764
+ if (out.skills) renderSkills(out.skills);
765
+ $("skillsNote").textContent = out.note || "Saved.";
766
+ closeEditor();
767
+ } catch { $("skillsNote").textContent = "Could not reach the server."; }
768
+ }
769
+
770
+ function wireSkills() {
771
+ $("skillsBody").addEventListener("click", (e) => {
772
+ const b = e.target.closest("button[data-action]");
773
+ if (!b) return;
774
+ const { name, action } = b.dataset;
775
+ if (action === "edit") { openEditor(name, b.dataset.enigma === "1"); return; }
776
+ if (action === "remove" && !confirm('Remove the external skill "' + name + '" from all agents? This deletes it from disk.')) return;
777
+ postSkill(name, action);
778
+ });
779
+ $("seSave").addEventListener("click", saveEditor);
780
+ $("seCancel").addEventListener("click", closeEditor);
781
+ $("skillsCheck").addEventListener("click", () => postSkill("*", "check-updates"));
782
+ }
783
+
784
+ let skillsLoaded = false;
785
+ async function loadSkills() {
786
+ try {
787
+ const res = await fetch("/api/skills", { cache: "no-store" });
788
+ renderSkills((await res.json()).skills || []);
789
+ } catch { $("skillsBody").innerHTML = '<div class="empty" style="padding:24px 0">Skills unavailable.</div>'; }
790
+ }
791
+
792
+ // --- hash routing between the Savings, Skills and Settings subpages ---
793
+ function route() {
794
+ const h = (location.hash || "").replace(/^#\/?/, "");
795
+ const v = (h === "settings" || h === "skills") ? h : "savings";
796
+ $("view-savings").style.display = v === "savings" ? "" : "none";
797
+ $("view-skills").style.display = v === "skills" ? "" : "none";
798
+ $("view-settings").style.display = v === "settings" ? "" : "none";
799
+ document.querySelectorAll(".tab").forEach((t) => t.classList.toggle("active", t.dataset.view === v));
800
+ if (v === "settings" && !settingsLoaded) { settingsLoaded = true; loadSettings(); }
801
+ if (v === "skills" && !skillsLoaded) { skillsLoaded = true; loadSkills(); }
802
+ // The chart was sized while its view may have been hidden; nudge it on return.
803
+ if (v === "savings" && chart) { try { applyRange(); } catch { /* not ready */ } }
804
+ }
805
+
637
806
  initChart();
638
807
  poll();
639
808
  setInterval(poll, POLL_MS);
640
809
  document.addEventListener("visibilitychange", () => { if (!document.hidden) poll(); });
641
810
  wireSettings();
642
- loadSettings();
811
+ wireSkills();
812
+ window.addEventListener("hashchange", route);
813
+ route();
643
814
  </script>
644
815
  </body>
645
816
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enigmax/dashboard",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Local browser dashboard UI for enigma: the static page and chart assets enigma serves on its loopback dashboard (savings, real tool usage, in-browser settings). Installed on demand by enigma-cli; not a runtime dependency.",
5
5
  "type": "module",
6
6
  "files": [