@bakery-framework/plugin-dashboard 2.0.0-alpha.8 → 2.0.0-rc.1

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.
package/README.md CHANGED
@@ -29,16 +29,16 @@ export default defineConfig({
29
29
  knows who its users are, so it decides: `authorize` returns true to allow a
30
30
  request through. Ship it without one and you are exposing your database browser.
31
31
 
32
- `enabled: false` keeps it out of a build entirely — the documented way to
32
+ `enabled: false` keeps it out of a build entirely, the documented way to
33
33
  disable it in production.
34
34
 
35
35
  ## License
36
36
 
37
- MIT with the Commons Clause v1.0 — see [LICENSE](./LICENSE).
37
+ MIT with the Commons Clause v1.0. See [LICENSE](./LICENSE).
38
38
 
39
- **Not an OSI-approved licence.** The Commons Clause removes the right to *sell*
40
- the software — meaning to charge for a product or service whose value derives
39
+ **Not an OSI-approved license.** The Commons Clause removes the right to *sell*
40
+ the software: meaning to charge for a product or service whose value derives
41
41
  substantially from it, hosting and support included. Everything else the MIT
42
- licence grants is unchanged: use it, modify it, ship it inside your own product.
43
- If your organisation only permits OSI-approved dependencies, this will not pass
42
+ license grants is unchanged: use it, modify it, ship it inside your own product.
43
+ If your organization only permits OSI-approved dependencies, this will not pass
44
44
  that check.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bakery-framework/plugin-dashboard",
3
- "version": "2.0.0-alpha.8",
3
+ "version": "2.0.0-rc.1",
4
4
  "description": "Bakery dashboard plugin.",
5
5
  "keywords": [
6
6
  "bakery",
@@ -33,11 +33,13 @@
33
33
  "!src/tests"
34
34
  ],
35
35
  "dependencies": {
36
- "@bakery-framework/core": "^2.0.0-alpha.8",
37
- "@bakery-framework/orm": "^2.0.0-alpha.8",
38
- "@bakery-framework/plugin-analytics": "^2.0.0-alpha.8"
36
+ "@bakery-framework/core": "^2.0.0-rc.1",
37
+ "@bakery-framework/plugin-analytics": "^2.0.0-rc.1"
38
+ },
39
+ "devDependencies": {
40
+ "@bakery-framework/orm": "^2.0.0-rc.1"
39
41
  },
40
42
  "engines": {
41
- "bun": ">=1.3.14"
43
+ "bun": ">=1.4.0"
42
44
  }
43
45
  }
@@ -1,24 +1,21 @@
1
- import { refreshShimmerCache } from './parts/effects'
2
1
  import { clearLogs, initLogsWebSocket, toggleLogsPlay } from './parts/logs'
3
2
  import {
4
3
  changeSessionPageSize,
5
4
  loadSessions,
6
5
  nextSessionPage,
7
- openSessionKeyEditor,
8
6
  prevSessionPage,
9
7
  queueSessionSearch,
10
8
  revokeSession,
11
9
  sessionKeyAction,
12
10
  } from './parts/sessions'
11
+ import { bindSparklineTooltips } from './parts/sparkline-tooltip'
13
12
  import {
14
- bindSparklineTooltips,
15
13
  changePagesFilter,
16
14
  changeTimescale,
17
15
  initAnalyticsWebSocket,
18
16
  loadStats,
19
17
  resetAnalytics,
20
18
  } from './parts/stats'
21
- import { SegmentedProgress } from './parts/utils'
22
19
 
23
20
  declare const match: any
24
21
 
@@ -51,15 +48,13 @@ function switchTab(tabId: string) {
51
48
  const label = document.querySelector(`.tab-btn.active span:last-child`)
52
49
  if (crumb && label) crumb.textContent = label.textContent
53
50
 
54
- // No `database` entry: that panel is static markup now — a link to the
55
- // explorer at `/_db` — so there is nothing to fetch when it is shown.
51
+ // No `database` entry: that panel is static markup now (a link to the
52
+ // explorer at `/_db`), so there is nothing to fetch when it is shown.
56
53
  match(tabId, {
57
54
  sessions: loadSessions,
58
55
  logs: initLogsWebSocket,
59
56
  'top-pages': () => loadStats(true),
60
57
  })
61
-
62
- refreshShimmerCache()
63
58
  }
64
59
 
65
60
  window.addEventListener('click', e => {
@@ -76,7 +71,6 @@ window.addEventListener('click', e => {
76
71
  })
77
72
 
78
73
  const w = window as any
79
- w.SegmentedProgress = SegmentedProgress
80
74
  w.switchTab = switchTab
81
75
  w.resetAnalytics = resetAnalytics
82
76
  w.changePagesFilter = changePagesFilter
@@ -90,7 +84,6 @@ w.prevSessionPage = prevSessionPage
90
84
  w.nextSessionPage = nextSessionPage
91
85
  w.changeSessionPageSize = changeSessionPageSize
92
86
  w.sessionKeyAction = sessionKeyAction
93
- w.openSessionKeyEditor = openSessionKeyEditor
94
87
 
95
88
  w.initLogsWebSocket = initLogsWebSocket
96
89
  w.toggleLogsPlay = toggleLogsPlay
@@ -6,8 +6,7 @@ export let logsPaused = false
6
6
  /**
7
7
  * The console keeps the most recent rows only. It previously appended
8
8
  * without ever removing, so a long-lived dashboard tab on a chatty server
9
- * grew the DOM without bound — and every append also triggered a shimmer
10
- * cache refresh, so the per-line cost climbed with the backlog.
9
+ * grew the DOM without bound.
11
10
  */
12
11
  const MAX_LOG_ROWS = 500
13
12
 
@@ -50,10 +49,10 @@ function renderLogEntry(cEl: HTMLElement, parsed: any) {
50
49
  logRow.style.borderBottom = '1px solid rgba(255, 255, 255, 0.02)'
51
50
 
52
51
  // `level` and `by` arrive over the websocket. LiveReloadHandler rebroadcasts
53
- // client_log frames from any connected client, so both are untrusted —
52
+ // client_log frames from any connected client, so both are untrusted:
54
53
  // `payload` was already escaped by colorizeHtml, these were not.
55
54
  logRow.innerHTML = `
56
- <span style="color: var(--text-secondary); margin-right: 0.5rem;">[${escapeHTML(String(timestamp))}]</span>
55
+ <span style="color: var(--text-muted); margin-right: 0.5rem;">[${escapeHTML(String(timestamp))}]</span>
57
56
  <span style="color: ${levelColor}; font-weight: bold; margin-right: 0.5rem;">[${escapeHTML(String(level))}]</span>
58
57
  <span style="color: #60a5fa; font-weight: 500; margin-right: 0.5rem;">${escapeHTML(String(by))}:</span>
59
58
  <span style="color: #f1f5f9; white-space: pre-wrap;">${colorizeHtml(payload)}</span>
@@ -75,15 +74,22 @@ export function initLogsWebSocket() {
75
74
  const consoleEl = document.getElementById('logs-console')
76
75
  if (!consoleEl) return
77
76
  consoleEl.innerHTML =
78
- '<div style="color: var(--text-secondary);">Connecting to server log stream...</div>'
77
+ '<div style="color: var(--text-muted);">Connecting to server log stream...</div>'
79
78
 
80
79
  try {
81
- logsWs = new WebSocket(getWebSocketUrl('/_livereload'))
80
+ // `/_dashboard/logs`, not `/_livereload`. The live-reload socket is
81
+ // registered only under `DEV`, so this panel answered 400 on every
82
+ // production server and sat on "Connecting..." for ever. The console has
83
+ // its own socket now, behind the console's own door.
84
+ logsWs = new WebSocket(getWebSocketUrl('/_dashboard/logs'))
82
85
 
83
86
  logsWs.onopen = () => {
84
87
  consoleEl.innerHTML =
85
- '<div style="color: var(--accent-green); display: flex; align-items: center; gap: 0.25rem;"><iconify-icon icon="lucide:check-circle-2" style="font-size: 1.1rem;"></iconify-icon><span>Connected to logs pipeline. Listening for events...</span></div>'
86
- logsWs?.send(JSON.stringify({ type: 'subscribe_logger' }))
88
+ '<div style="color: var(--ok); display: flex; align-items: center; gap: 0.25rem;"><span>Connected to logs pipeline. Listening for events...</span></div>'
89
+ // No `subscribe_logger` frame: membership is the handler's `open`, so
90
+ // there is nothing to ask for. That message was `LiveReloadHandler`'s
91
+ // protocol, and it is what an app page still sends to forward its own
92
+ // console in development.
87
93
  }
88
94
 
89
95
  logsWs.onmessage = event => {
@@ -103,13 +109,13 @@ export function initLogsWebSocket() {
103
109
  const logRow = document.createElement('div')
104
110
  logRow.style.color = '#f59e0b'
105
111
  logRow.innerHTML =
106
- '<span style="display: flex; align-items: center; gap: 0.25rem;"><iconify-icon icon="lucide:alert-triangle" style="font-size: 1rem;"></iconify-icon><span>Logs pipeline disconnected. Reconnecting in 3s...</span></span>'
112
+ '<span style="display: flex; align-items: center; gap: 0.25rem;"><span>Logs pipeline disconnected. Reconnecting in 3s...</span></span>'
107
113
  consoleEl.appendChild(logRow)
108
114
  setTimeout(initLogsWebSocket, 3000)
109
115
  }
110
116
  } catch (_err) {
111
117
  consoleEl.innerHTML =
112
- '<div style="color: var(--accent-red);">Failed to establish log stream connection.</div>'
118
+ '<div style="color: var(--danger);">Failed to establish log stream connection.</div>'
113
119
  }
114
120
  }
115
121
 
@@ -118,8 +124,8 @@ export function toggleLogsPlay() {
118
124
  const btn = document.getElementById('btn-logs-play')
119
125
  if (btn) {
120
126
  btn.innerHTML = logsPaused
121
- ? '<iconify-icon icon="lucide:play" style="font-size: 1.1rem;"></iconify-icon><span>Resume</span>'
122
- : '<iconify-icon icon="lucide:pause" style="font-size: 1.1rem;"></iconify-icon><span>Pause</span>'
127
+ ? '<span>Resume</span>'
128
+ : '<span>Pause</span>'
123
129
  btn.classList.toggle('btn-success', logsPaused)
124
130
  }
125
131
  }
@@ -128,5 +134,5 @@ export function clearLogs() {
128
134
  const consoleEl = document.getElementById('logs-console')
129
135
  if (consoleEl)
130
136
  consoleEl.innerHTML =
131
- '<div style="color: var(--text-secondary);">Console cleared.</div>'
137
+ '<div style="color: var(--text-muted);">Console cleared.</div>'
132
138
  }
@@ -0,0 +1,251 @@
1
+ /**
2
+ * What the console charts are, and the state that says how they are being
3
+ * shown.
4
+ *
5
+ * Split out of `stats.ts`, which was 999 lines holding four separate jobs: this
6
+ * catalog, the canvas drawing, the tooltip DOM, and the live feed that drives
7
+ * all three. Nothing here touches a canvas or a socket, which is what makes it
8
+ * the piece the other three can each depend on without depending on each other.
9
+ */
10
+
11
+ import { setText } from './utils'
12
+
13
+ /**
14
+ * One row per sparkline. The nine metrics were previously written out
15
+ * longhand at seven separate sites: the history arrays, the tracker record,
16
+ * the tooltip configs, `drawAllSparklines`, the two incremental update paths,
17
+ * the full-history path and `changeTimescale`, so adding or renaming one
18
+ * meant seven coordinated edits, and the two update paths had already drifted
19
+ * apart in how they coerce missing values.
20
+ *
21
+ * That drift is preserved deliberately, not flattened: `zero` says whether a
22
+ * history point's missing value reads as `0`, and `live` covers the two
23
+ * metrics whose live payload differs from their history payload (`ping` uses
24
+ * `??`, and `memoryUsed` arrives as a `"12 MB"` string over the socket).
25
+ */
26
+ export interface Metric {
27
+ /** Tracker key, and the prefix of the `-min` / `-max` / `-avg` element ids. */
28
+ key: string
29
+ /** Canvas element id. */
30
+ canvas: string
31
+ /** `.chart-card` id, for the metrics that blur when analytics is inactive. */
32
+ card?: string
33
+ /** `.card-sub` caption, before the timescale label is appended. */
34
+ sub: string
35
+ /** Property name on a history point and on the live stats payload. */
36
+ field: string
37
+ /** Suffix in the hover tooltip; trimmed for the min/max/avg readout. */
38
+ unit: string
39
+ stroke: string
40
+ fill: string
41
+ /** Whether a missing history value reads as `0`. */
42
+ zero: boolean
43
+ /** Live-payload reader, where it differs from the history reader. */
44
+ live?: (s: any) => number
45
+ history: number[]
46
+ }
47
+
48
+ export const METRICS: Metric[] = [
49
+ {
50
+ key: 'ping',
51
+ canvas: 'canvas-ping',
52
+ sub: 'Server self-check ping latency',
53
+ field: 'ping',
54
+ unit: 'ms',
55
+ stroke: '#f43f5e',
56
+ fill: 'rgba(244, 63, 94, 0.25)',
57
+ zero: true,
58
+ live: s => s.ping ?? 0,
59
+ history: [],
60
+ },
61
+ {
62
+ key: 'memory',
63
+ canvas: 'canvas-memory',
64
+ sub: 'Heap/RSS RAM consumption',
65
+ field: 'memoryUsed',
66
+ unit: ' MB',
67
+ stroke: '#3b82f6',
68
+ fill: 'rgba(59, 130, 246, 0.25)',
69
+ zero: false,
70
+ live: s => parseFloat(s.memoryUsed) || 0,
71
+ history: [],
72
+ },
73
+ {
74
+ key: 'loggers',
75
+ canvas: 'canvas-loggers',
76
+ sub: 'Active client logger tunnels',
77
+ field: 'activeLoggers',
78
+ unit: '',
79
+ stroke: '#10b981',
80
+ fill: 'rgba(16, 185, 129, 0.25)',
81
+ zero: false,
82
+ history: [],
83
+ },
84
+ {
85
+ key: 'sessions',
86
+ canvas: 'canvas-sessions',
87
+ sub: 'In-memory active user sessions',
88
+ field: 'activeSessions',
89
+ unit: '',
90
+ stroke: '#fbbf24',
91
+ fill: 'rgba(251, 191, 36, 0.25)',
92
+ zero: false,
93
+ history: [],
94
+ },
95
+ {
96
+ key: 'pageHits',
97
+ canvas: 'canvas-route-hits',
98
+ card: 'chart-route-hits',
99
+ sub: 'Application page requests',
100
+ field: 'pageHits',
101
+ unit: '',
102
+ stroke: '#06b6d4',
103
+ fill: 'rgba(6, 182, 212, 0.25)',
104
+ zero: true,
105
+ history: [],
106
+ },
107
+ {
108
+ key: 'apiHits',
109
+ canvas: 'canvas-api-hits',
110
+ card: 'chart-api-hits',
111
+ sub: 'API endpoint requests',
112
+ field: 'apiHits',
113
+ unit: '',
114
+ stroke: '#8b5cf6',
115
+ fill: 'rgba(139, 92, 246, 0.25)',
116
+ zero: true,
117
+ history: [],
118
+ },
119
+ {
120
+ key: 'uniqueRequests',
121
+ canvas: 'canvas-unique-requests',
122
+ card: 'chart-unique-requests',
123
+ sub: 'Distinct request signatures',
124
+ field: 'uniqueRequests',
125
+ unit: '',
126
+ stroke: '#f97316',
127
+ fill: 'rgba(249, 115, 22, 0.25)',
128
+ zero: true,
129
+ history: [],
130
+ },
131
+ {
132
+ key: 'errorPageHits',
133
+ canvas: 'canvas-error-page-hits',
134
+ card: 'chart-error-page-hits',
135
+ sub: 'Custom error page renders',
136
+ field: 'errorPageHits',
137
+ unit: '',
138
+ stroke: '#ef4444',
139
+ fill: 'rgba(239, 68, 68, 0.25)',
140
+ zero: true,
141
+ history: [],
142
+ },
143
+ ]
144
+
145
+ export const METRIC_BY_KEY = new Map(METRICS.map(m => [m.key, m]))
146
+
147
+ /** The "have we loaded any history yet" probe; every metric fills together. */
148
+ export const memoryHistory = METRIC_BY_KEY.get('memory')!.history
149
+
150
+ /** A history point's value for this metric. */
151
+ export function readPoint(m: Metric, point: any): number {
152
+ return m.zero ? point[m.field] || 0 : point[m.field]
153
+ }
154
+
155
+ /** The live per-second payload's value for this metric. */
156
+ export function readLive(m: Metric, s: any): number {
157
+ return m.live ? m.live(s) : s[m.field] || 0
158
+ }
159
+
160
+ /**
161
+ * Which window the charts are showing.
162
+ *
163
+ * Here rather than in `stats.ts` because two modules read it (the live feed
164
+ * and the tooltip's age labels), and only one writes it. An `export let`
165
+ * cannot be assigned from outside the module that declares it, so the write
166
+ * goes through `setActiveTimescale` and every reader still sees the live
167
+ * binding.
168
+ */
169
+ export let activeTimescale = '1m'
170
+
171
+ export function setActiveTimescale(next: string): void {
172
+ activeTimescale = next
173
+ }
174
+
175
+ /**
176
+ * The timescale shape, as the browser sees it.
177
+ *
178
+ * **This is a second copy of `analytics/src/timescale.ts`, and it has to be.**
179
+ * That module is the one source for the server, and importing it here compiles
180
+ * (`tsconfig` paths resolve it), but does not *run*: this file is bundled for
181
+ * the browser and served as a classic `<script>`, so a cross-package specifier
182
+ * survives as a bare `import` that the page cannot execute. It took the whole
183
+ * console down silently, because `Bun.build` reports success and nothing
184
+ * requested the page.
185
+ *
186
+ * So the duplication is deliberate and the agreement is a test rather than a
187
+ * hope: `timescale-agreement.test.ts` imports both and fails if they diverge.
188
+ * That is most of what collapsing the five original copies bought, and it is
189
+ * the part that survives a boundary the bundler genuinely cannot cross.
190
+ *
191
+ * Same derivation as the other side: a bucket is the window divided by the
192
+ * points, and a sample is a second of the bucket.
193
+ */
194
+ const DAY_MS = 86_400_000
195
+
196
+ export const TIMESCALE_SHAPE: Record<
197
+ string,
198
+ { windowMs: number; points: number }
199
+ > = {
200
+ '1m': { windowMs: 60_000, points: 60 },
201
+ '1h': { windowMs: 3_600_000, points: 60 },
202
+ '1d': { windowMs: DAY_MS, points: 48 },
203
+ '7d': { windowMs: 7 * DAY_MS, points: 28 },
204
+ '30d': { windowMs: 30 * DAY_MS, points: 30 },
205
+ }
206
+
207
+ /** Unknown falls back to the narrowest window, as the server's copy does. */
208
+ function shapeOf(timescale: string) {
209
+ return TIMESCALE_SHAPE[timescale] ?? TIMESCALE_SHAPE['1m']!
210
+ }
211
+
212
+ /** How much time one chart point covers. */
213
+ export function getTimescaleIntervalMs(timescale: string): number {
214
+ const { windowMs, points } = shapeOf(timescale)
215
+ return windowMs / points
216
+ }
217
+
218
+ /** How many points a timescale is drawn as. See `TIMESCALE_SHAPE`. */
219
+ export function getTimescaleLimit(timescale: string): number {
220
+ return shapeOf(timescale).points
221
+ }
222
+
223
+ export interface Tracker {
224
+ min: number
225
+ max: number
226
+ sum: number
227
+ count: number
228
+ }
229
+
230
+ export function emptyTracker(): Tracker {
231
+ return { min: Infinity, max: -Infinity, sum: 0, count: 0 }
232
+ }
233
+
234
+ export const trackers: Record<string, Tracker> = Object.fromEntries(
235
+ METRICS.map(m => [m.key, emptyTracker()]),
236
+ )
237
+
238
+ export function updateTracker(key: string, val: number) {
239
+ if (val === null || val === undefined || Number.isNaN(val)) return
240
+ const t = trackers[key]
241
+ if (val < t.min) t.min = val
242
+ if (val > t.max) t.max = val
243
+ t.sum += val
244
+ t.count += 1
245
+ const avg = t.sum / t.count
246
+
247
+ const suffix = (METRIC_BY_KEY.get(key)?.unit ?? '').trim()
248
+ setText(`${key}-min`, `${t.min.toFixed(0)} ${suffix}`)
249
+ setText(`${key}-max`, `${t.max.toFixed(0)} ${suffix}`)
250
+ setText(`${key}-avg`, `${avg.toFixed(1)} ${suffix}`)
251
+ }
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  ICON_DELETE,
3
- ICON_EDIT,
4
3
  icon,
5
4
  postJson,
6
5
  setEmpty,
@@ -77,36 +76,21 @@ function renderKVRows(
77
76
  visible.forEach(([k, v]) => {
78
77
  const row = document.createElement('div')
79
78
  row.style.cssText =
80
- 'display:flex;align-items:center;gap:0.5rem;background:rgba(255,255,255,0.04);border:1px solid var(--border-color);border-radius:0.375rem;padding:0.3rem 0.6rem;'
79
+ 'display:flex;align-items:center;gap:0.5rem;background:rgba(255,255,255,0.04);border:1px solid var(--border);border-radius:0.375rem;padding:0.3rem 0.6rem;'
81
80
 
82
81
  const keyEl = document.createElement('span')
83
82
  keyEl.style.cssText =
84
- 'font-size:0.8rem;font-weight:600;color:var(--text-secondary);min-width:120px;font-family:monospace;'
83
+ 'font-size:0.8rem;font-weight:600;color:var(--text-muted);min-width:120px;font-family:monospace;'
85
84
  keyEl.innerText = k
86
85
 
87
86
  const valEl = document.createElement('span')
88
87
  valEl.style.cssText =
89
- 'font-size:0.8rem;color:var(--text-primary);flex:1;font-family:monospace;word-break:break-all;'
88
+ 'font-size:0.8rem;color:var(--text);flex:1;font-family:monospace;word-break:break-all;'
90
89
  valEl.innerText = is.object(v) ? JSON.stringify(v) : String(v)
91
90
 
92
- const editBtn = document.createElement('button')
93
- editBtn.style.cssText =
94
- 'background:none;border:none;cursor:pointer;color:var(--text-secondary);font-size:0.85rem;padding:0.1rem 0.25rem;border-radius:0.25rem;transition:color 0.15s;'
95
- editBtn.title = 'Edit value'
96
- editBtn.innerHTML = icon(ICON_EDIT, '0.95rem')
97
- editBtn.onmouseenter = () => (editBtn.style.color = 'var(--text-primary)')
98
- editBtn.onmouseleave = () => (editBtn.style.color = 'var(--text-secondary)')
99
- editBtn.onclick = () =>
100
- openSessionKeyEditor(
101
- sId,
102
- k,
103
- String(is.object(v) ? JSON.stringify(v) : v),
104
- () => loadSessions(),
105
- )
106
-
107
91
  const delBtn = document.createElement('button')
108
92
  delBtn.style.cssText =
109
- 'background:none;border:none;cursor:pointer;color:var(--accent-red);font-size:0.85rem;padding:0.1rem 0.25rem;border-radius:0.25rem;opacity:0.7;transition:opacity 0.15s;'
93
+ 'background:none;border:none;cursor:pointer;color:var(--danger);font-size:0.85rem;padding:0.1rem 0.25rem;border-radius:0.25rem;opacity:0.7;transition:opacity 0.15s;'
110
94
  delBtn.title = 'Delete key'
111
95
  delBtn.innerHTML = icon(ICON_DELETE, '0.95rem')
112
96
  delBtn.onmouseenter = () => (delBtn.style.opacity = '1')
@@ -118,7 +102,6 @@ function renderKVRows(
118
102
 
119
103
  row.appendChild(keyEl)
120
104
  row.appendChild(valEl)
121
- row.appendChild(editBtn)
122
105
  row.appendChild(delBtn)
123
106
  kvSection.appendChild(row)
124
107
  })
@@ -126,12 +109,12 @@ function renderKVRows(
126
109
  if (entries.length > SHOW_LIMIT) {
127
110
  const toggle = document.createElement('button')
128
111
  toggle.style.cssText =
129
- 'font-size:0.75rem;color:var(--text-secondary);background:none;border:none;cursor:pointer;text-align:left;padding:0.1rem 0;margin-top:0.1rem;transition:color 0.15s;'
112
+ 'font-size:0.75rem;color:var(--text-muted);background:none;border:none;cursor:pointer;text-align:left;padding:0.1rem 0;margin-top:0.1rem;transition:color 0.15s;'
130
113
  toggle.innerText = showAll
131
114
  ? `▲ Show fewer`
132
115
  : `▼ Show all ${entries.length} keys`
133
- toggle.onmouseenter = () => (toggle.style.color = 'var(--text-primary)')
134
- toggle.onmouseleave = () => (toggle.style.color = 'var(--text-secondary)')
116
+ toggle.onmouseenter = () => (toggle.style.color = 'var(--text)')
117
+ toggle.onmouseleave = () => (toggle.style.color = 'var(--text-muted)')
135
118
  toggle.onclick = () => renderKVRows(kvSection, entries, sId, !showAll)
136
119
  kvSection.appendChild(toggle)
137
120
  }
@@ -139,21 +122,11 @@ function renderKVRows(
139
122
  if (entries.length === 0) {
140
123
  const empty = document.createElement('span')
141
124
  empty.style.cssText =
142
- 'font-size:0.8rem;color:var(--text-secondary);font-style:italic;'
125
+ 'font-size:0.8rem;color:var(--text-muted);font-style:italic;'
143
126
  empty.innerText = 'No data stored in this session.'
144
127
  kvSection.appendChild(empty)
145
128
  }
146
129
 
147
- const addRow = document.createElement('div')
148
- addRow.style.cssText = 'margin-top:0.35rem;'
149
- const addBtn = document.createElement('button')
150
- addBtn.className = 'btn btn-secondary'
151
- addBtn.style.cssText = 'font-size:0.75rem;padding:0.25rem 0.65rem;'
152
- addBtn.innerText = '+ Add Key'
153
- addBtn.onclick = () =>
154
- openSessionKeyEditor(sId, '', '', () => loadSessions(), true)
155
- addRow.appendChild(addBtn)
156
- kvSection.appendChild(addRow)
157
130
  }
158
131
 
159
132
  function renderSessionCard(s: any): HTMLElement {
@@ -179,10 +152,10 @@ function renderSessionCard(s: any): HTMLElement {
179
152
  const expiresAt = new Date((s.accessedAt || Date.now()) + ttl)
180
153
  const info = document.createElement('div')
181
154
  info.style.cssText =
182
- 'font-size:0.8rem;color:var(--text-secondary);display:flex;gap:1.5rem;margin-bottom:0.5rem;'
155
+ 'font-size:0.8rem;color:var(--text-muted);display:flex;gap:1.5rem;margin-bottom:0.5rem;'
183
156
  info.innerHTML = `
184
- <span>Last Accessed: <strong style="color:var(--text-primary)">${accessedAt.toLocaleTimeString()}</strong></span>
185
- <span>Expires: <strong style="color:var(--text-primary)">${expiresAt.toLocaleString()}</strong></span>
157
+ <span>Last Accessed: <strong style="color:var(--text)">${accessedAt.toLocaleTimeString()}</strong></span>
158
+ <span>Expires: <strong style="color:var(--text)">${expiresAt.toLocaleString()}</strong></span>
186
159
  `
187
160
 
188
161
  const kvSection = document.createElement('div')
@@ -278,92 +251,21 @@ export async function sessionKeyAction(
278
251
  }
279
252
  }
280
253
 
281
- export function openSessionKeyEditor(
282
- sessionId: string,
283
- key: string,
284
- currentValue: string,
285
- onDone: () => void,
286
- isNew = false,
287
- ) {
288
- document.getElementById('session-key-editor-overlay')?.remove()
289
-
290
- const overlay = document.createElement('div')
291
- overlay.id = 'session-key-editor-overlay'
292
- overlay.className = 'modal-overlay'
293
- overlay.style.zIndex = '200'
294
-
295
- const card = document.createElement('div')
296
- card.className = 'modal-card'
297
- card.style.maxWidth = '420px'
298
-
299
- // Session keys are written by application code, so their names can carry
300
- // whatever that code derived them from. Both the heading and the prefilled
301
- // input reach innerHTML unescaped before this — a stored payload in a
302
- // session key ran as the dashboard operator the moment they opened it.
303
- const safeKey = escapeHTML(String(key))
304
- card.innerHTML = `
305
- <div class="modal-header">
306
- <h3>${isNew ? 'Add Session Key' : `Edit Key: <code style="font-size:0.85em;font-weight:400;">${safeKey}</code>`}</h3>
307
- <button class="modal-close" id="skey-close">×</button>
308
- </div>
309
- <div style="display:flex;flex-direction:column;gap:0.75rem;">
310
- ${
311
- isNew
312
- ? `
313
- <div class="form-group">
314
- <label class="label">Key</label>
315
- <input class="input-field" id="skey-key-input" type="text" placeholder="e.g. userId" value="${safeKey}" />
316
- </div>
317
- `
318
- : ''
319
- }
320
- <div class="form-group">
321
- <label class="label">Value <span style="font-size:0.7rem;color:var(--text-secondary);">(string)</span></label>
322
- <input class="input-field" id="skey-val-input" type="text" placeholder="value" value="${escapeHTML(currentValue)}" />
323
- </div>
324
- </div>
325
- <div class="modal-actions">
326
- <button class="btn btn-secondary" id="skey-cancel">Cancel</button>
327
- <button class="btn" id="skey-save">Save</button>
328
- </div>
329
- `
330
-
331
- overlay.appendChild(card)
332
- document.body.appendChild(overlay)
333
-
334
- const close = () => overlay.remove()
335
- document.getElementById('skey-close')!.onclick = close
336
- document.getElementById('skey-cancel')!.onclick = close
337
- overlay.addEventListener('click', e => {
338
- if (e.target === overlay) close()
339
- })
340
-
341
- document.getElementById('skey-save')!.onclick = async () => {
342
- const finalKey = isNew
343
- ? (
344
- document.getElementById('skey-key-input') as HTMLInputElement
345
- )?.value?.trim()
346
- : key
347
- const val =
348
- (document.getElementById('skey-val-input') as HTMLInputElement)?.value ??
349
- ''
350
- if (!finalKey) {
351
- alert('Key cannot be empty.')
352
- return
353
- }
354
- await sessionKeyAction(sessionId, finalKey, val)
355
- close()
356
- onDone()
357
- }
358
-
359
- setTimeout(() => {
360
- const el = document.getElementById(
361
- isNew ? 'skey-key-input' : 'skey-val-input',
362
- ) as HTMLInputElement | null
363
- el?.focus()
364
- el?.select()
365
- }, 50)
366
- }
254
+ /*
255
+ * `openSessionKeyEditor` was here, and it could not be opened.
256
+ *
257
+ * It built its overlay with `className = 'modal-overlay'`, and the sheet
258
+ * gives that rule `display: none` - only `.modal-overlay.active` is
259
+ * `display: flex`, and nothing in this package ever added `active`. Checked
260
+ * against a running console: clicking Edit created the element, computed
261
+ * `display: none`, rendered 0x0. So the editor has never opened, and the
262
+ * two buttons that called it did nothing at all.
263
+ *
264
+ * Restoring it would mean deciding what a session-key editor should be,
265
+ * which is a design question rather than a missing class name, and 2.0.0 is
266
+ * not the release to answer it in. What is left on this panel is what works:
267
+ * listing sessions, revoking one, and deleting a key.
268
+ */
367
269
 
368
270
  export async function revokeSession(sessionId: string) {
369
271
  if (!confirm('Are you sure you want to revoke this session?')) return