@prsm/devtools 1.1.0 → 1.2.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-DWT8hGf_.js"></script>
17
+ <link rel="stylesheet" crossorigin href="./assets/index-B7_KPm6_.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.2.0",
4
4
  "description": "Read-only Express middleware dashboard for observing @prsm infrastructure at runtime",
5
5
  "type": "module",
6
6
  "exports": {
@@ -32,11 +32,14 @@
32
32
  },
33
33
  "devDependencies": {
34
34
  "@prsm/cells": "^1.2.0",
35
- "@prsm/cron": "^1.0.3",
36
- "@prsm/limit": "^1.1.1",
35
+ "@prsm/cron": "^1.1.0",
36
+ "@prsm/limit": "^1.2.0",
37
+ "@prsm/lock": "^1.1.0",
37
38
  "@prsm/queue": "^3.0.8",
39
+ "@prsm/workflow": "^3.1.0",
38
40
  "cors": "^2.8.6",
39
- "dotenv": "^16.4.7"
41
+ "dotenv": "^16.4.7",
42
+ "pg": "^8.13.1"
40
43
  },
41
44
  "engines": {
42
45
  "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,10 @@ 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 } = options
33
33
  const cellGraphs = normalizeCellGraphs(options.cells)
34
34
  const sseClients = new Set()
35
+ const jsonBody = json()
35
36
 
36
37
  function broadcast(event, data) {
37
38
  const msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`
@@ -84,6 +85,7 @@ export function prsmDevtools(options = {}) {
84
85
  workflow: !!workflow,
85
86
  realtime: !!realtime,
86
87
  cells: cellGraphs ? Object.keys(cellGraphs) : [],
88
+ lock: lock ? Object.keys(lock) : [],
87
89
  })
88
90
  })
89
91
 
@@ -111,6 +113,18 @@ export function prsmDevtools(options = {}) {
111
113
  }))
112
114
  res.json({ jobs })
113
115
  })
116
+
117
+ router.post('/api/cron/:name/run', async (req, res) => {
118
+ if (typeof cron.run !== 'function') {
119
+ return res.status(400).json({ error: 'This @prsm/cron version does not support manual runs' })
120
+ }
121
+ try {
122
+ const result = await cron.run(req.params.name)
123
+ res.json(result)
124
+ } catch (err) {
125
+ res.status(400).json({ error: err.message })
126
+ }
127
+ })
114
128
  }
115
129
 
116
130
  if (limit) {
@@ -118,13 +132,45 @@ export function prsmDevtools(options = {}) {
118
132
  res.json({ limiters: Object.keys(limit) })
119
133
  })
120
134
 
135
+ router.get('/api/limits/:name/keys', async (req, res) => {
136
+ const limiter = limit[req.params.name]
137
+ if (!limiter) return res.status(404).json({ error: 'Limiter not found' })
138
+ if (!limiter.keys) {
139
+ return res.status(400).json({ error: 'This @prsm/limit version does not support key listing' })
140
+ }
141
+ try {
142
+ const n = req.query.limit ? Number(req.query.limit) : undefined
143
+ const keys = await limiter.keys(n ? { limit: n } : undefined)
144
+ res.json({ keys })
145
+ } catch (err) {
146
+ res.status(500).json({ error: err.message })
147
+ }
148
+ })
149
+
121
150
  router.get('/api/limits/:name/peek/:key', async (req, res) => {
122
151
  const limiter = limit[req.params.name]
123
152
  if (!limiter) return res.status(404).json({ error: 'Limiter not found' })
124
153
  if (!limiter.peek) return res.status(400).json({ error: 'Limiter does not support peek' })
125
154
 
126
- const result = await limiter.peek(req.params.key)
127
- res.json(result)
155
+ try {
156
+ const result = await limiter.peek(req.params.key)
157
+ res.json(result)
158
+ } catch (err) {
159
+ res.status(500).json({ error: err.message })
160
+ }
161
+ })
162
+
163
+ router.post('/api/limits/:name/reset/:key', async (req, res) => {
164
+ const limiter = limit[req.params.name]
165
+ if (!limiter) return res.status(404).json({ error: 'Limiter not found' })
166
+ if (!limiter.reset) return res.status(400).json({ error: 'Limiter does not support reset' })
167
+
168
+ try {
169
+ await limiter.reset(req.params.key)
170
+ res.json({ ok: true })
171
+ } catch (err) {
172
+ res.status(500).json({ error: err.message })
173
+ }
128
174
  })
129
175
  }
130
176
 
@@ -168,6 +214,47 @@ export function prsmDevtools(options = {}) {
168
214
  if (!execution) return res.status(404).json({ error: 'Execution not found' })
169
215
  res.json({ execution })
170
216
  })
217
+
218
+ router.post('/api/workflow/start', jsonBody, async (req, res) => {
219
+ const { name, version, input } = req.body || {}
220
+ if (!name || typeof name !== 'string') {
221
+ return res.status(400).json({ error: 'name is required' })
222
+ }
223
+ try {
224
+ const execution = await workflow.start(name, input ?? {}, version ? { version } : undefined)
225
+ res.json({ id: execution.id })
226
+ } catch (err) {
227
+ res.status(400).json({ error: err.message })
228
+ }
229
+ })
230
+
231
+ router.post('/api/workflow/executions/:id/signal', jsonBody, async (req, res) => {
232
+ try {
233
+ const execution = await workflow.signal(req.params.id, req.body?.payload ?? {})
234
+ res.json({ execution })
235
+ } catch (err) {
236
+ const status = err.name === 'AlreadySignaledError' ? 409 : 400
237
+ res.status(status).json({ error: err.message })
238
+ }
239
+ })
240
+
241
+ router.post('/api/workflow/executions/:id/cancel', jsonBody, async (req, res) => {
242
+ try {
243
+ const execution = await workflow.cancel(req.params.id, req.body?.reason)
244
+ res.json({ execution })
245
+ } catch (err) {
246
+ res.status(400).json({ error: err.message })
247
+ }
248
+ })
249
+
250
+ router.post('/api/workflow/executions/:id/resume', async (req, res) => {
251
+ try {
252
+ const execution = await workflow.resume(req.params.id)
253
+ res.json({ execution })
254
+ } catch (err) {
255
+ res.status(400).json({ error: err.message })
256
+ }
257
+ })
171
258
  }
172
259
 
173
260
  if (realtime) {
@@ -374,6 +461,46 @@ export function prsmDevtools(options = {}) {
374
461
  })
375
462
  }
376
463
 
464
+ if (lock) {
465
+ router.get('/api/locks', async (_req, res) => {
466
+ const managers = []
467
+ for (const [name, manager] of Object.entries(lock)) {
468
+ const kind = typeof manager.renew === 'function' ? 'semaphore' : 'mutex'
469
+ try {
470
+ managers.push({ name, kind, locks: await manager.list() })
471
+ } catch (err) {
472
+ managers.push({ name, kind, locks: [], error: err.message })
473
+ }
474
+ }
475
+ res.json({ managers })
476
+ })
477
+
478
+ router.get('/api/locks/:name', async (req, res) => {
479
+ const manager = lock[req.params.name]
480
+ if (!manager) return res.status(404).json({ error: 'Lock manager not found' })
481
+ try {
482
+ const kind = typeof manager.renew === 'function' ? 'semaphore' : 'mutex'
483
+ const locks = await manager.list()
484
+ res.json({ name: req.params.name, kind, locks })
485
+ } catch (err) {
486
+ res.status(500).json({ error: err.message })
487
+ }
488
+ })
489
+
490
+ router.post('/api/locks/:name/release', jsonBody, async (req, res) => {
491
+ const manager = lock[req.params.name]
492
+ if (!manager) return res.status(404).json({ error: 'Lock manager not found' })
493
+ const { key, id } = req.body || {}
494
+ if (!key || !id) return res.status(400).json({ error: 'key and id are required' })
495
+ try {
496
+ const released = await manager.release(key, id)
497
+ res.json({ released })
498
+ } catch (err) {
499
+ res.status(500).json({ error: err.message })
500
+ }
501
+ })
502
+ }
503
+
377
504
  if (existsSync(clientDir)) {
378
505
  const indexPath = resolve(clientDir, 'index.html')
379
506
  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}