@prsm/devtools 1.1.0 → 1.3.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.
@@ -4,11 +4,17 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>prsm devtools</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Space+Mono:wght@400;700&display=swap"
11
+ rel="stylesheet"
12
+ />
7
13
  <style>
8
- body { margin: 0; background: #0a0a0a; }
14
+ body { margin: 0; background: #ffffff; }
9
15
  </style>
10
- <script type="module" crossorigin src="./assets/index-LvnrgtvZ.js"></script>
11
- <link rel="stylesheet" crossorigin href="./assets/index-Dj0vLuDk.css">
16
+ <script type="module" crossorigin src="./assets/index-DYVuye_Q.js"></script>
17
+ <link rel="stylesheet" crossorigin href="./assets/index-BEgM9Z3B.css">
12
18
  </head>
13
19
  <body>
14
20
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prsm/devtools",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Read-only Express middleware dashboard for observing @prsm infrastructure at runtime",
5
5
  "type": "module",
6
6
  "exports": {
@@ -31,12 +31,17 @@
31
31
  "express": "^4.21.0"
32
32
  },
33
33
  "devDependencies": {
34
+ "@prsm/cache": "^1.0.2",
34
35
  "@prsm/cells": "^1.2.0",
35
- "@prsm/cron": "^1.0.3",
36
- "@prsm/limit": "^1.1.1",
36
+ "@prsm/cron": "^1.1.0",
37
+ "@prsm/limit": "^1.2.0",
38
+ "@prsm/lock": "^1.1.0",
37
39
  "@prsm/queue": "^3.0.8",
40
+ "@prsm/realtime": "^1.0.5",
41
+ "@prsm/workflow": "^3.1.0",
38
42
  "cors": "^2.8.6",
39
- "dotenv": "^16.4.7"
43
+ "dotenv": "^16.4.7",
44
+ "pg": "^8.13.1"
40
45
  },
41
46
  "engines": {
42
47
  "node": ">=18"
package/src/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Router, static as serveStatic } from 'express'
1
+ import { Router, static as serveStatic, json } from 'express'
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { resolve, dirname } from 'node:path'
4
4
  import { existsSync, readFileSync } from 'node:fs'
@@ -29,9 +29,23 @@ function patternToString(p) {
29
29
  */
30
30
  export function prsmDevtools(options = {}) {
31
31
  const router = Router()
32
- const { queue, cron, limit, workflow, realtime } = options
32
+ const { queue, cron, limit, workflow, realtime, lock, cache } = options
33
+ const connectionDisplay = typeof options.connectionDisplay === 'function' ? options.connectionDisplay : null
34
+
35
+ function displayFor(metadata) {
36
+ if (!connectionDisplay) return { label: null, sublabel: null }
37
+ try {
38
+ const out = connectionDisplay(metadata) ?? {}
39
+ const label = typeof out.label === 'string' && out.label.length ? out.label : null
40
+ const sublabel = typeof out.sublabel === 'string' && out.sublabel.length ? out.sublabel : null
41
+ return { label, sublabel }
42
+ } catch {
43
+ return { label: null, sublabel: null }
44
+ }
45
+ }
33
46
  const cellGraphs = normalizeCellGraphs(options.cells)
34
47
  const sseClients = new Set()
48
+ const jsonBody = json()
35
49
 
36
50
  function broadcast(event, data) {
37
51
  const msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`
@@ -76,6 +90,15 @@ export function prsmDevtools(options = {}) {
76
90
  }
77
91
  }
78
92
 
93
+ if (cache) {
94
+ for (const [name, c] of Object.entries(cache)) {
95
+ const events = ['hit', 'miss', 'set', 'del', 'invalidate', 'refresh', 'stampede:lead', 'stampede:wait', 'stampede:result', 'stampede:timeout', 'error']
96
+ for (const ev of events) {
97
+ c.on(ev, (data) => broadcast(`cache:${ev}`, { cache: name, ...(data ?? {}) }))
98
+ }
99
+ }
100
+ }
101
+
79
102
  router.get('/api/config', (_req, res) => {
80
103
  res.json({
81
104
  queue: !!queue,
@@ -84,9 +107,27 @@ export function prsmDevtools(options = {}) {
84
107
  workflow: !!workflow,
85
108
  realtime: !!realtime,
86
109
  cells: cellGraphs ? Object.keys(cellGraphs) : [],
110
+ lock: lock ? Object.keys(lock) : [],
111
+ cache: cache ? Object.keys(cache) : [],
87
112
  })
88
113
  })
89
114
 
115
+ if (cache) {
116
+ router.get('/api/cache', (_req, res) => {
117
+ const out = {}
118
+ for (const [name, c] of Object.entries(cache)) {
119
+ out[name] = c.stats()
120
+ }
121
+ res.json(out)
122
+ })
123
+
124
+ router.get('/api/cache/:name', (req, res) => {
125
+ const c = cache[req.params.name]
126
+ if (!c) return res.status(404).json({ error: 'cache not found' })
127
+ res.json({ name: req.params.name, stats: c.stats() })
128
+ })
129
+ }
130
+
90
131
  router.get('/api/events', (req, res) => {
91
132
  res.setHeader('Content-Type', 'text/event-stream')
92
133
  res.setHeader('Cache-Control', 'no-cache')
@@ -111,6 +152,18 @@ export function prsmDevtools(options = {}) {
111
152
  }))
112
153
  res.json({ jobs })
113
154
  })
155
+
156
+ router.post('/api/cron/:name/run', async (req, res) => {
157
+ if (typeof cron.run !== 'function') {
158
+ return res.status(400).json({ error: 'This @prsm/cron version does not support manual runs' })
159
+ }
160
+ try {
161
+ const result = await cron.run(req.params.name)
162
+ res.json(result)
163
+ } catch (err) {
164
+ res.status(400).json({ error: err.message })
165
+ }
166
+ })
114
167
  }
115
168
 
116
169
  if (limit) {
@@ -118,13 +171,45 @@ export function prsmDevtools(options = {}) {
118
171
  res.json({ limiters: Object.keys(limit) })
119
172
  })
120
173
 
174
+ router.get('/api/limits/:name/keys', async (req, res) => {
175
+ const limiter = limit[req.params.name]
176
+ if (!limiter) return res.status(404).json({ error: 'Limiter not found' })
177
+ if (!limiter.keys) {
178
+ return res.status(400).json({ error: 'This @prsm/limit version does not support key listing' })
179
+ }
180
+ try {
181
+ const n = req.query.limit ? Number(req.query.limit) : undefined
182
+ const keys = await limiter.keys(n ? { limit: n } : undefined)
183
+ res.json({ keys })
184
+ } catch (err) {
185
+ res.status(500).json({ error: err.message })
186
+ }
187
+ })
188
+
121
189
  router.get('/api/limits/:name/peek/:key', async (req, res) => {
122
190
  const limiter = limit[req.params.name]
123
191
  if (!limiter) return res.status(404).json({ error: 'Limiter not found' })
124
192
  if (!limiter.peek) return res.status(400).json({ error: 'Limiter does not support peek' })
125
193
 
126
- const result = await limiter.peek(req.params.key)
127
- res.json(result)
194
+ try {
195
+ const result = await limiter.peek(req.params.key)
196
+ res.json(result)
197
+ } catch (err) {
198
+ res.status(500).json({ error: err.message })
199
+ }
200
+ })
201
+
202
+ router.post('/api/limits/:name/reset/:key', async (req, res) => {
203
+ const limiter = limit[req.params.name]
204
+ if (!limiter) return res.status(404).json({ error: 'Limiter not found' })
205
+ if (!limiter.reset) return res.status(400).json({ error: 'Limiter does not support reset' })
206
+
207
+ try {
208
+ await limiter.reset(req.params.key)
209
+ res.json({ ok: true })
210
+ } catch (err) {
211
+ res.status(500).json({ error: err.message })
212
+ }
128
213
  })
129
214
  }
130
215
 
@@ -168,6 +253,47 @@ export function prsmDevtools(options = {}) {
168
253
  if (!execution) return res.status(404).json({ error: 'Execution not found' })
169
254
  res.json({ execution })
170
255
  })
256
+
257
+ router.post('/api/workflow/start', jsonBody, async (req, res) => {
258
+ const { name, version, input } = req.body || {}
259
+ if (!name || typeof name !== 'string') {
260
+ return res.status(400).json({ error: 'name is required' })
261
+ }
262
+ try {
263
+ const execution = await workflow.start(name, input ?? {}, version ? { version } : undefined)
264
+ res.json({ id: execution.id })
265
+ } catch (err) {
266
+ res.status(400).json({ error: err.message })
267
+ }
268
+ })
269
+
270
+ router.post('/api/workflow/executions/:id/signal', jsonBody, async (req, res) => {
271
+ try {
272
+ const execution = await workflow.signal(req.params.id, req.body?.payload ?? {})
273
+ res.json({ execution })
274
+ } catch (err) {
275
+ const status = err.name === 'AlreadySignaledError' ? 409 : 400
276
+ res.status(status).json({ error: err.message })
277
+ }
278
+ })
279
+
280
+ router.post('/api/workflow/executions/:id/cancel', jsonBody, async (req, res) => {
281
+ try {
282
+ const execution = await workflow.cancel(req.params.id, req.body?.reason)
283
+ res.json({ execution })
284
+ } catch (err) {
285
+ res.status(400).json({ error: err.message })
286
+ }
287
+ })
288
+
289
+ router.post('/api/workflow/executions/:id/resume', async (req, res) => {
290
+ try {
291
+ const execution = await workflow.resume(req.params.id)
292
+ res.json({ execution })
293
+ } catch (err) {
294
+ res.status(400).json({ error: err.message })
295
+ }
296
+ })
171
297
  }
172
298
 
173
299
  if (realtime) {
@@ -194,6 +320,7 @@ export function prsmDevtools(options = {}) {
194
320
  return {
195
321
  id,
196
322
  metadata,
323
+ ...displayFor(metadata),
197
324
  local: !!local,
198
325
  latency: local?.latency?.ms ?? null,
199
326
  alive: local?.alive ?? null,
@@ -283,6 +410,7 @@ export function prsmDevtools(options = {}) {
283
410
  res.json({
284
411
  id,
285
412
  metadata,
413
+ ...displayFor(metadata),
286
414
  rooms,
287
415
  presence,
288
416
  channels,
@@ -374,6 +502,46 @@ export function prsmDevtools(options = {}) {
374
502
  })
375
503
  }
376
504
 
505
+ if (lock) {
506
+ router.get('/api/locks', async (_req, res) => {
507
+ const managers = []
508
+ for (const [name, manager] of Object.entries(lock)) {
509
+ const kind = typeof manager.renew === 'function' ? 'semaphore' : 'mutex'
510
+ try {
511
+ managers.push({ name, kind, locks: await manager.list() })
512
+ } catch (err) {
513
+ managers.push({ name, kind, locks: [], error: err.message })
514
+ }
515
+ }
516
+ res.json({ managers })
517
+ })
518
+
519
+ router.get('/api/locks/:name', async (req, res) => {
520
+ const manager = lock[req.params.name]
521
+ if (!manager) return res.status(404).json({ error: 'Lock manager not found' })
522
+ try {
523
+ const kind = typeof manager.renew === 'function' ? 'semaphore' : 'mutex'
524
+ const locks = await manager.list()
525
+ res.json({ name: req.params.name, kind, locks })
526
+ } catch (err) {
527
+ res.status(500).json({ error: err.message })
528
+ }
529
+ })
530
+
531
+ router.post('/api/locks/:name/release', jsonBody, async (req, res) => {
532
+ const manager = lock[req.params.name]
533
+ if (!manager) return res.status(404).json({ error: 'Lock manager not found' })
534
+ const { key, id } = req.body || {}
535
+ if (!key || !id) return res.status(400).json({ error: 'key and id are required' })
536
+ try {
537
+ const released = await manager.release(key, id)
538
+ res.json({ released })
539
+ } catch (err) {
540
+ res.status(500).json({ error: err.message })
541
+ }
542
+ })
543
+ }
544
+
377
545
  if (existsSync(clientDir)) {
378
546
  const indexPath = resolve(clientDir, 'index.html')
379
547
  const indexHtml = readFileSync(indexPath, 'utf8')
@@ -1 +0,0 @@
1
- :root{--bg: #0a0a0a;--bg-surface: #111111;--bg-raised: #1a1a1a;--bg-hover: #222222;--bg-active: #2a2a2a;--border: #2a2a2a;--border-subtle: #1e1e1e;--text: #d4d4d4;--text-bright: #e8e8e8;--text-muted: #555555;--accent: #34d399;--accent-dim: rgba(52, 211, 153, .12);--accent-text: #2dd4a2;--color-blue: #6ad;--color-red: #c55;--color-yellow: #b93;--color-green: #5a9;--syn-string: #a5d6a7;--syn-number: #4dd0e1;--syn-boolean: #ce93d8;--syn-null: #666666;--syn-key: #b0b0b0;--syn-bracket: #555555}*{box-sizing:border-box;margin:0;padding:0}body{font-family:SF Mono,Fira Code,JetBrains Mono,Cascadia Code,monospace;font-size:12px;line-height:1.5;color:var(--text);background:var(--bg);-webkit-font-smoothing:antialiased}::selection{background:var(--accent-dim);color:var(--accent-text)}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:#3a3a3a}.app{display:flex;flex-direction:column;height:100vh;overflow:hidden}.top-bar{display:flex;align-items:center;justify-content:space-between;padding:0 16px;height:40px;border-bottom:1px solid var(--border);background:var(--bg-surface);flex-shrink:0}.top-bar-left{display:flex;align-items:center;gap:12px}.logo{font-size:11px;font-weight:600;letter-spacing:.5px;text-transform:uppercase;color:var(--text-muted)}.logo span{color:var(--accent)}.top-bar-right{display:flex;align-items:center;gap:12px}.nav-bar{display:flex;align-items:center;gap:0;border-bottom:1px solid var(--border);background:var(--bg-surface);flex-shrink:0;padding:0 16px}.nav-bar a{padding:8px 16px;font-size:11px;color:var(--text-muted);cursor:pointer;border-bottom:2px solid transparent;-webkit-user-select:none;user-select:none;text-decoration:none}.nav-bar a:hover{color:var(--text)}.nav-bar a.active{color:var(--accent-text);border-bottom-color:var(--accent)}.page-scroll{flex:1;overflow-y:auto}.page-content{max-width:1100px;padding:20px 24px 40px}.page-content-wide{padding:20px 24px 40px}.tab-bar{display:flex;align-items:center;gap:0;border-bottom:1px solid var(--border);background:var(--bg-surface);flex-shrink:0;padding:0 16px}.tab{padding:8px 16px;font-size:11px;color:var(--text-muted);cursor:pointer;border-bottom:2px solid transparent;-webkit-user-select:none;user-select:none}.tab:hover{color:var(--text)}.tab.active{color:var(--accent-text);border-bottom-color:var(--accent)}.tab .count{margin-left:6px;font-size:10px;color:var(--text-muted)}.tab.active .count{color:var(--accent)}.sidebar{width:280px;min-width:280px;border-right:1px solid var(--border);overflow-y:auto;background:var(--bg-surface)}.content{flex:1;overflow-y:auto;padding:16px}.sidebar-section{border-bottom:1px solid var(--border-subtle)}.sidebar-header{padding:8px 12px;font-size:10px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-muted);background:var(--bg)}.sidebar-item{display:flex;align-items:center;justify-content:space-between;padding:6px 12px;cursor:pointer;border-left:2px solid transparent}.sidebar-item:hover{background:var(--bg-hover)}.sidebar-item.active{background:var(--accent-dim);border-left-color:var(--accent)}.sidebar-item .label{font-size:11px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sidebar-item.active .label{color:var(--accent-text)}.sidebar-item .meta{font-size:10px;color:var(--text-muted);flex-shrink:0;margin-left:8px}.badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;font-size:10px;border-radius:9px;background:#1f1f1f;color:#999}.badge.accent{background:var(--accent-dim);color:var(--accent)}.section{margin-bottom:20px}.section-title{font-size:10px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-muted);margin-bottom:8px}.card{background:var(--bg-surface);border:1px solid var(--border);border-radius:4px;overflow:hidden}.card+.card{margin-top:8px}.card-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:var(--bg);border-bottom:1px solid var(--border-subtle);font-size:11px}.card-header .name{color:var(--text-bright)}.card-body{padding:8px 12px}.kv-row{display:flex;align-items:baseline;padding:2px 0;font-size:11px}.kv-key{color:var(--text-muted);min-width:100px;flex-shrink:0}.kv-value{color:var(--text);word-break:break-all}.member-row{display:flex;align-items:center;justify-content:space-between;padding:4px 12px;font-size:11px;border-bottom:1px solid var(--border-subtle)}.member-row:last-child{border-bottom:none}.member-id{color:var(--text);font-size:11px}.member-presence{font-size:10px;color:var(--accent)}.tag{display:inline-block;padding:1px 6px;font-size:10px;border-radius:3px;background:#1f1f1f;color:#999;margin:1px 2px}.tag.accent{background:var(--accent-dim);color:var(--accent)}.empty{padding:24px;text-align:center;color:var(--text-muted);font-size:11px}.view-hint{font-size:11px;color:var(--text-muted);margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--border-subtle)}.no-presence{font-size:10px;color:#333}.pattern-list{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.json-view{font-size:11px;line-height:1.6;white-space:pre-wrap;word-break:break-all}.json-view .shiki{background:transparent!important;padding:0;margin:0}.json-view .shiki code{font-family:inherit;font-size:inherit}.select{background:var(--bg-raised);border:1px solid var(--border);color:var(--text);font-family:inherit;font-size:11px;padding:4px 8px;border-radius:3px;outline:none;cursor:pointer}.select:focus{border-color:var(--accent)}.conn-link{cursor:pointer;text-decoration:underline;text-decoration-color:var(--border);text-underline-offset:2px}.conn-link:hover{color:var(--accent-text);text-decoration-color:var(--accent)}.exposed-row{display:flex;align-items:center;flex-wrap:wrap;gap:3px;padding:4px 12px;border-bottom:1px solid var(--border-subtle)}.exposed-row:last-child{border-bottom:none}.exposed-label{font-size:10px;color:var(--text-muted);min-width:70px;flex-shrink:0}.pulse{width:6px;height:6px;border-radius:50%;background:var(--accent)}.pulse.disconnected{background:#ef4444}.instance-id{font-size:10px;color:var(--text-muted)}section[data-v-5ee7e46a]{margin-top:28px}.subsystems[data-v-5ee7e46a]{display:flex;gap:8px;margin-bottom:20px}.chip[data-v-5ee7e46a]{padding:5px 14px;font-size:11px;font-weight:500;color:var(--text-muted);background:var(--bg-surface);border:1px solid var(--border);border-radius:4px;font-family:inherit;cursor:pointer}.chip.on[data-v-5ee7e46a]{color:var(--text);border-color:#333}.chip.off[data-v-5ee7e46a]{color:var(--text-muted);border-color:var(--border-subtle);opacity:.55}.chip[data-v-5ee7e46a]:disabled{cursor:default;opacity:.35}.stream[data-v-5ee7e46a]{background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:4px;max-height:520px;overflow-y:auto}.event-row[data-v-5ee7e46a]{display:flex;gap:0;align-items:center;padding:0;border-bottom:1px solid var(--bg-raised);font-size:11px}.event-row[data-v-5ee7e46a]:last-child{border-bottom:none}.event-source[data-v-5ee7e46a]{width:72px;padding:6px 8px;color:var(--text-muted);text-align:right;border-right:1px solid var(--bg-raised);flex-shrink:0}.event-action[data-v-5ee7e46a]{width:152px;padding:6px 8px;color:var(--text-muted);flex-shrink:0}.event-action.complete[data-v-5ee7e46a],.event-action.fire[data-v-5ee7e46a],.event-action.execution-succeeded[data-v-5ee7e46a],.event-action.step-succeeded[data-v-5ee7e46a]{color:var(--color-green)}.event-action.step-routed[data-v-5ee7e46a]{color:var(--color-blue)}.event-action.failed[data-v-5ee7e46a],.event-action.error[data-v-5ee7e46a],.event-action.execution-failed[data-v-5ee7e46a],.event-action.step-failed[data-v-5ee7e46a],.event-action.execution-lease-lost[data-v-5ee7e46a]{color:var(--color-red)}.event-action.retry[data-v-5ee7e46a],.event-action.step-retry[data-v-5ee7e46a]{color:var(--color-yellow)}.event-data[data-v-5ee7e46a]{flex:1;min-width:0;padding:6px 8px;color:#666;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.event-time[data-v-5ee7e46a]{width:80px;padding:6px 8px;color:var(--text-muted);text-align:right;flex-shrink:0}.gauges[data-v-74a2dfa0]{display:flex;gap:12px;margin-bottom:8px}.gauge[data-v-74a2dfa0]{flex:1;padding:16px;background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:4px}.gauge-value[data-v-74a2dfa0]{display:block;font-size:28px;font-weight:600;color:var(--text-muted)}.gauge-value.lit[data-v-74a2dfa0]{color:var(--color-blue)}.gauge-value.warn[data-v-74a2dfa0]{color:var(--color-red)}.gauge-label[data-v-74a2dfa0]{display:block;font-size:10px;color:var(--text-muted);margin-top:4px;letter-spacing:.3px;text-transform:uppercase}.session-note[data-v-74a2dfa0]{font-size:10px;color:#333;margin-bottom:24px}section[data-v-74a2dfa0]{margin-top:24px}.stream[data-v-74a2dfa0]{background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:4px;max-height:400px;overflow-y:auto}.event-row[data-v-74a2dfa0]{display:flex;gap:0;align-items:center;border-bottom:1px solid var(--bg-raised);font-size:11px}.event-row[data-v-74a2dfa0]:last-child{border-bottom:none}.event-action[data-v-74a2dfa0]{width:72px;padding:6px 10px;color:var(--text-muted);flex-shrink:0}.event-action.complete[data-v-74a2dfa0]{color:var(--color-green)}.event-action.failed[data-v-74a2dfa0]{color:var(--color-red)}.event-action.retry[data-v-74a2dfa0]{color:var(--color-yellow)}.event-action.new[data-v-74a2dfa0]{color:var(--text-muted)}.event-action.drain[data-v-74a2dfa0]{color:#666}.event-data[data-v-74a2dfa0]{flex:1;padding:6px 8px;color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.event-time[data-v-74a2dfa0]{width:80px;padding:6px 8px;color:#333;text-align:right;flex-shrink:0}.job-row[data-v-6d545d29]{display:flex;align-items:center;padding:0;border-bottom:1px solid var(--border-subtle);font-size:11px}.job-row[data-v-6d545d29]:last-child{border-bottom:none}.job-row.header[data-v-6d545d29]{font-size:10px;color:var(--text-muted);letter-spacing:.3px;text-transform:uppercase;border-bottom:1px solid var(--border)}.job-row.header span[data-v-6d545d29]{padding:8px 12px}.col-name[data-v-6d545d29]{flex:1;padding:10px 12px}.col-next[data-v-6d545d29]{width:100px;padding:10px 12px;color:#666;text-align:right}.col-in[data-v-6d545d29]{width:60px;padding:10px 12px;color:var(--text-muted);text-align:right}.col-in.soon[data-v-6d545d29]{color:var(--color-blue)}.stream[data-v-6d545d29]{background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:4px;max-height:400px;overflow-y:auto}.event-row[data-v-6d545d29]{display:flex;align-items:center;border-bottom:1px solid var(--bg-raised);font-size:11px}.event-row[data-v-6d545d29]:last-child{border-bottom:none}.event-action[data-v-6d545d29]{width:52px;padding:6px 10px;color:var(--text-muted);flex-shrink:0}.event-action.fire[data-v-6d545d29]{color:var(--color-green)}.event-action.error[data-v-6d545d29]{color:var(--color-red)}.event-name[data-v-6d545d29]{flex:1;padding:6px 8px;color:#666}.event-time[data-v-6d545d29]{width:80px;padding:6px 8px;color:#333;text-align:right;flex-shrink:0}.text-view[data-v-5a4270db]{font:12px SF Mono,monospace;color:var(--text);white-space:pre-wrap;word-break:break-word;margin:0}.limiter-list[data-v-22c855ec]{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:24px}section[data-v-22c855ec]{margin-top:24px}.desc[data-v-22c855ec]{font-size:11px;color:var(--text-muted);margin-bottom:12px}.peek-form[data-v-22c855ec]{display:flex;gap:8px}.input[data-v-22c855ec]{padding:6px 10px;background:var(--bg-surface);border:1px solid var(--border);border-radius:4px;font-size:11px;color:var(--text);flex:1;font-family:inherit;outline:none}.input[data-v-22c855ec]::placeholder{color:#333}.input[data-v-22c855ec]:focus{border-color:var(--accent)}.btn[data-v-22c855ec]{padding:6px 16px;background:var(--bg-raised);color:#999;border:1px solid var(--border);border-radius:4px;cursor:pointer;font-size:11px;font-family:inherit}.btn[data-v-22c855ec]:hover{color:var(--text)}.btn[data-v-22c855ec]:disabled{opacity:.3;cursor:default}.error[data-v-22c855ec]{margin-top:10px;color:var(--color-red);font-size:11px}.graph-shell[data-v-6098c89d]{background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:4px;overflow:hidden;position:relative;cursor:grab;touch-action:none}.graph-shell[data-v-6098c89d]:active{cursor:grabbing}.graph-shell svg[data-v-6098c89d]{user-select:none;-webkit-user-select:none}.graph-shell.fullscreen[data-v-6098c89d]{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;border-radius:0;border:none;background:var(--bg)}.graph-controls[data-v-6098c89d]{position:absolute;top:10px;right:10px;z-index:2;display:flex;gap:4px}.graph-controls button[data-v-6098c89d]{padding:4px 10px;background:var(--bg-raised);border:1px solid var(--border);border-radius:4px;color:#888;font-size:11px;font-family:inherit;cursor:pointer}.graph-controls button[data-v-6098c89d]:hover{background:var(--bg-hover);color:#ccc}.graph[data-v-6098c89d]{width:100%;min-height:420px;display:block}.fullscreen .graph[data-v-6098c89d]{min-height:100vh}.edge[data-v-6098c89d]{fill:none;stroke:#232323;stroke-width:3}.edge-seen[data-v-6098c89d]{stroke:#3b3b3b}.edge-active[data-v-6098c89d]{stroke:#8fb3ff}.edge-label[data-v-6098c89d]{fill:#4a4a4a;font-size:11px;font-family:inherit}.node[data-v-6098c89d]{cursor:pointer}.node rect[data-v-6098c89d]{fill:#151515;stroke:#262626;stroke-width:1.5}.node.selected rect[data-v-6098c89d]{stroke:#6b7280}.node.current rect[data-v-6098c89d],.node.running rect[data-v-6098c89d]{stroke:#5b8cff}.node.succeeded rect[data-v-6098c89d],.node.decision-success rect[data-v-6098c89d]{stroke:#3d8f63}.node.failed rect[data-v-6098c89d]{stroke:#a14a4a}.node.activity .node-type[data-v-6098c89d]{fill:#7aa2f7}.node.decision .node-type[data-v-6098c89d]{fill:#e0af68}.node.succeed .node-type[data-v-6098c89d]{fill:#73daca}.node.fail .node-type[data-v-6098c89d]{fill:#f7768e}.node-name[data-v-6098c89d]{fill:var(--text-bright);font-size:13px;font-weight:600}.node-type[data-v-6098c89d],.node-meta[data-v-6098c89d]{font-family:inherit;font-size:11px}.node-meta[data-v-6098c89d]{fill:#5f5f5f}.workflow-layout[data-v-124a0c49]{display:grid;grid-template-columns:280px 1fr;gap:16px}.wf-sidebar[data-v-124a0c49]{display:flex;flex-direction:column;gap:8px}.wf-sidebar .card.active[data-v-124a0c49]{border-color:var(--accent)}.detail[data-v-124a0c49]{display:flex;flex-direction:column;gap:16px}.headline[data-v-124a0c49]{display:flex;justify-content:space-between;align-items:flex-end}.headline-name[data-v-124a0c49]{font-size:14px;font-weight:600;color:var(--text-bright)}.headline-sub[data-v-124a0c49]{margin-top:2px;color:var(--text-muted);font-size:11px}.headline-stats[data-v-124a0c49]{display:flex;gap:10px;color:var(--text-muted);font-size:11px}.filters[data-v-24631062]{display:flex;gap:8px;margin-bottom:16px}.execution-layout[data-v-24631062]{display:grid;grid-template-columns:240px minmax(0,1fr) 340px;gap:14px;align-items:start}.list[data-v-24631062]{background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:4px;overflow:hidden;max-height:calc(100vh - 240px);overflow-y:auto}.execution-row[data-v-24631062]{padding:10px 12px;border-bottom:1px solid var(--bg-raised);cursor:pointer}.execution-row[data-v-24631062]:last-child{border-bottom:none}.execution-row.active[data-v-24631062]{background:var(--bg-raised)}.execution-main[data-v-24631062]{display:flex;justify-content:space-between;gap:8px}.execution-workflow[data-v-24631062]{color:var(--text-bright);font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.execution-id[data-v-24631062]{color:#666;font-size:11px;flex-shrink:0}.execution-meta[data-v-24631062]{display:flex;justify-content:space-between;gap:6px;margin-top:4px;color:var(--text-muted);font-size:10px}.execution-row.running .execution-workflow[data-v-24631062]{color:#8fb3ff}.execution-row.failed .execution-workflow[data-v-24631062]{color:#d87a7a}.execution-row.succeeded .execution-workflow[data-v-24631062]{color:#7ec49b}.headline[data-v-24631062]{margin-bottom:14px}.headline-name[data-v-24631062]{font-size:14px;font-weight:600;color:var(--text-bright)}.headline-sub[data-v-24631062]{margin-top:2px;color:var(--text-muted);font-size:11px}.center[data-v-24631062]{display:flex;flex-direction:column;gap:12px;min-width:0}.inspector[data-v-24631062]{display:flex;flex-direction:column;gap:10px;max-height:calc(100vh - 240px);overflow-y:auto}.step-hint[data-v-24631062]{color:#383838;font-size:10px;margin-bottom:8px;font-style:italic}.timeline[data-v-24631062]{max-height:420px;overflow-y:auto}.timeline-row[data-v-24631062]{display:flex;gap:6px;align-items:baseline;border-bottom:1px solid var(--border-subtle);padding:6px 12px;font-size:10px;white-space:nowrap}.timeline-row[data-v-24631062]:last-child{border-bottom:none}.timeline-type[data-v-24631062]{color:#b1b1b1;flex-shrink:0}.timeline-step[data-v-24631062]{color:#666;flex:1;overflow:hidden;text-overflow:ellipsis}.timeline-time[data-v-24631062]{color:var(--text-muted);flex-shrink:0}.realtime-shell[data-v-d6b8bed2]{display:flex;flex-direction:column;flex:1;overflow:hidden}.rt-main[data-v-d6b8bed2]{display:flex;flex:1;overflow:hidden}.tab-bar-right[data-v-d6b8bed2]{margin-left:auto;display:flex;align-items:center;gap:8px}.tab-bar a[data-v-d6b8bed2]{text-decoration:none}.cells-page[data-v-183abe97]{display:flex;flex-direction:column;gap:12px}.cells-toolbar[data-v-183abe97]{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.left-group[data-v-183abe97]{display:flex;align-items:center;gap:12px}.graph-tabs[data-v-183abe97]{display:flex;border:1px solid var(--border);border-radius:4px;overflow:hidden}.graph-tabs button[data-v-183abe97]{padding:6px 14px;background:var(--bg-surface);color:var(--text-muted);border:none;font:inherit;font-size:11px;cursor:pointer;border-right:1px solid var(--border)}.graph-tabs button[data-v-183abe97]:last-child{border-right:none}.graph-tabs button.active[data-v-183abe97]{background:var(--accent-dim);color:var(--accent-text)}.graph-tabs button[data-v-183abe97]:hover:not(.active){background:var(--bg-hover);color:var(--text)}.view-toggle[data-v-183abe97]{display:flex;border:1px solid var(--border);border-radius:4px;overflow:hidden}.view-toggle button[data-v-183abe97]{padding:6px 14px;background:var(--bg-surface);color:var(--text-muted);border:none;font:inherit;font-size:11px;cursor:pointer;border-right:1px solid var(--border)}.view-toggle button[data-v-183abe97]:last-child{border-right:none}.view-toggle button.active[data-v-183abe97]{background:var(--accent-dim);color:var(--accent-text)}.view-toggle button[data-v-183abe97]:hover:not(.active){background:var(--bg-hover);color:var(--text)}.stats[data-v-183abe97]{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--text-muted)}.dot[data-v-183abe97]{width:3px;height:3px;background:var(--text-muted);border-radius:50%}.cells-layout[data-v-183abe97]{display:flex;flex-direction:column;gap:12px;min-height:0}.cells-main[data-v-183abe97]{min-width:0;overflow:auto}.cells-detail[data-v-183abe97]{background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:4px;padding:12px}.detail-grid[data-v-183abe97]{display:grid;gap:10px;grid-template-columns:minmax(0,1fr) 240px}.detail-main[data-v-183abe97]{display:grid;gap:10px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));min-width:0}.detail-side[data-v-183abe97]{display:flex;flex-direction:column;gap:10px}.detail-card[data-v-183abe97]{background:var(--bg);border:1px solid var(--border-subtle);border-radius:3px;padding:10px;min-width:0;display:flex;flex-direction:column;gap:6px}.detail-card.span-2[data-v-183abe97]{grid-column:span 2}@media(max-width:900px){.detail-grid[data-v-183abe97]{grid-template-columns:minmax(0,1fr)}.detail-card.span-2[data-v-183abe97]{grid-column:span 1}}.scroll-y[data-v-183abe97]{max-height:220px;overflow-y:auto}.kv-list[data-v-183abe97]{display:flex;flex-direction:column;gap:8px}.kv-list-row[data-v-183abe97]{display:flex;flex-direction:column;gap:2px;min-width:0}.kv-list-key[data-v-183abe97]{font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px}.kv-list-val[data-v-183abe97]{font-size:11px;color:var(--text);word-break:break-word;font-family:SF Mono,monospace}.cell-row[data-v-183abe97]{display:flex;align-items:center;border-bottom:1px solid var(--border-subtle);font-size:11px;cursor:pointer}.cell-row[data-v-183abe97]:last-child{border-bottom:none}.cell-row[data-v-183abe97]:hover{background:var(--bg-hover)}.cell-row.selected[data-v-183abe97]{background:var(--accent-dim)}.cell-row.recent[data-v-183abe97]{animation:flash-183abe97 .8s ease-out}.cell-row.header[data-v-183abe97]{font-size:10px;color:var(--text-muted);letter-spacing:.3px;text-transform:uppercase;cursor:default;border-bottom:1px solid var(--border)}.cell-row.header[data-v-183abe97]:hover{background:transparent}@keyframes flash-183abe97{0%{background:var(--accent-dim)}to{background:transparent}}.col-name[data-v-183abe97]{flex:0 0 200px;padding:8px 12px;color:var(--text-bright)}.col-value[data-v-183abe97]{flex:1;padding:8px 12px;color:var(--text);font-family:SF Mono,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.col-deps[data-v-183abe97]{flex:0 0 200px;padding:8px 12px;color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.col-status[data-v-183abe97]{flex:0 0 80px;padding:8px 12px}.col-updated[data-v-183abe97]{flex:0 0 100px;padding:8px 12px;color:var(--text-muted);text-align:right}.graph-wrap[data-v-183abe97]{background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:4px;padding:8px;overflow:auto;width:100%;box-sizing:border-box}.graph-wrap svg[data-v-183abe97]{display:block}.node-group[data-v-183abe97]{cursor:pointer}.node-group rect[data-v-183abe97]{fill:var(--bg-raised);stroke:var(--border);stroke-width:1}.node-group:hover rect[data-v-183abe97]{stroke:var(--text-muted)}.node-group.selected rect[data-v-183abe97]{stroke:var(--accent);stroke-width:1.5}.node-group.recent rect[data-v-183abe97]{animation:pulse-rect-183abe97 .8s ease-out}@keyframes pulse-rect-183abe97{0%{fill:var(--accent-dim);stroke:var(--accent)}to{fill:var(--bg-raised);stroke:var(--border)}}.edge-line[data-v-183abe97]{stroke:var(--border)}.edge-line.recent[data-v-183abe97]{animation:pulse-edge-183abe97 .8s ease-out}@keyframes pulse-edge-183abe97{0%{stroke:var(--accent)}to{stroke:var(--border)}}.node-name[data-v-183abe97]{fill:var(--text-bright);font:600 11px SF Mono,monospace}.node-value[data-v-183abe97]{fill:var(--text);font:10px SF Mono,monospace}.detail-header[data-v-183abe97]{display:flex;align-items:center;gap:8px;padding-bottom:10px;margin-bottom:12px;border-bottom:1px solid var(--border)}.detail-name[data-v-183abe97]{flex:1;font-size:12px;color:var(--text-bright);font-weight:600}.detail-status[data-v-183abe97]{font-size:9px;text-transform:uppercase;letter-spacing:.5px}.detail-spacer[data-v-183abe97]{flex:1}.detail-close[data-v-183abe97]{cursor:pointer;color:var(--text-muted);font-size:16px;padding:0 4px}.detail-close[data-v-183abe97]:hover{color:var(--text-bright)}.detail-section[data-v-183abe97]{margin-bottom:14px}.detail-label[data-v-183abe97]{font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px}.detail-content[data-v-183abe97]{font-size:11px;color:var(--text);word-break:break-word}.detail-pre[data-v-183abe97]{font:11px SF Mono,monospace;color:var(--text);background:var(--bg);border:1px solid var(--border-subtle);border-radius:3px;padding:8px;white-space:pre-wrap;overflow-x:auto;max-height:220px}.detail-label[data-v-183abe97]{display:flex;align-items:center;gap:8px}.detail-label-meta[data-v-183abe97]{font-size:9px;color:var(--text);text-transform:none;letter-spacing:0;font-family:SF Mono,monospace}.history-disabled[data-v-183abe97]{font-size:10px;color:var(--text-muted);line-height:1.5}.history-disabled code[data-v-183abe97]{background:var(--bg-raised);color:var(--text);padding:1px 4px;border-radius:2px;font-size:10px}.history-list[data-v-183abe97]{display:flex;flex-direction:column;gap:2px;max-height:240px;overflow-y:auto}.history-entry[data-v-183abe97]{display:flex;gap:8px;align-items:center;font-size:10px;padding:3px 6px;background:var(--bg);border-radius:2px}.history-time[data-v-183abe97]{flex:0 0 70px;color:var(--text-muted)}.history-value[data-v-183abe97]{flex:1;color:var(--text);font-family:SF Mono,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.empty[data-v-183abe97]{padding:20px;text-align:center;color:var(--text-muted);font-size:11px}.empty-small[data-v-183abe97]{padding:6px;text-align:center;color:var(--text-muted);font-size:10px}