Skip to content

Commit 404d71a

Browse files
refactor(devtools): extract shared functions into src/functions/ to eliminate duplication
Moves duplicated badge, formatter, chart config, and constant definitions from 12 page scripts into shared TypeScript files, bundled and served as /bq-utils.js for global access in stx template expressions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bb5511b commit 404d71a

20 files changed

Lines changed: 1532 additions & 1294 deletions
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* Shared badge/status class utilities for the devtools dashboard.
3+
* These are bundled and served as globals for stx template expressions.
4+
*/
5+
6+
/**
7+
* Badge classes for job statuses (active = indigo).
8+
* Used by: jobs, index, batch-details, batches, group-details, groups
9+
*/
10+
export function badgeClass(status: string): string {
11+
const map: Record<string, string> = {
12+
completed: 'bg-emerald-500/15 text-emerald-500',
13+
finished: 'bg-emerald-500/15 text-emerald-500',
14+
failed: 'bg-red-500/15 text-red-500',
15+
processing: 'bg-indigo-500/15 text-indigo-400',
16+
active: 'bg-indigo-500/15 text-indigo-400',
17+
waiting: 'bg-amber-500/15 text-amber-500',
18+
pending: 'bg-amber-500/15 text-amber-500',
19+
delayed: 'bg-zinc-500/15 text-zinc-500',
20+
}
21+
return map[status] || 'bg-zinc-500/15 text-zinc-500'
22+
}
23+
24+
/**
25+
* Badge classes for queue health statuses (active = emerald).
26+
* Used by: queues, queue-details
27+
*/
28+
export function queueBadgeClass(status: string): string {
29+
const map: Record<string, string> = {
30+
active: 'bg-emerald-500/15 text-emerald-500',
31+
paused: 'bg-amber-500/15 text-amber-500',
32+
stopped: 'bg-red-500/15 text-red-500',
33+
completed: 'bg-emerald-500/15 text-emerald-500',
34+
failed: 'bg-red-500/15 text-red-500',
35+
waiting: 'bg-amber-500/15 text-amber-500',
36+
pending: 'bg-amber-500/15 text-amber-500',
37+
}
38+
return map[status] || 'bg-zinc-500/15 text-zinc-500'
39+
}
40+
41+
/**
42+
* Progress bar color based on batch/queue status.
43+
* Used by: batches
44+
*/
45+
export function progressBarColorClass(status: string): string {
46+
const map: Record<string, string> = {
47+
completed: 'bg-emerald-500',
48+
finished: 'bg-emerald-500',
49+
failed: 'bg-red-500',
50+
processing: 'bg-indigo-500',
51+
active: 'bg-indigo-500',
52+
pending: 'bg-amber-500',
53+
waiting: 'bg-amber-500',
54+
}
55+
return map[status] || 'bg-indigo-500'
56+
}
57+
58+
/**
59+
* Dot color for monitoring event types.
60+
* Used by: monitoring
61+
*/
62+
export function dotColor(dot: string): string {
63+
const map: Record<string, string> = {
64+
completed: 'bg-emerald-500',
65+
started: 'bg-indigo-500',
66+
failed: 'bg-red-500',
67+
queued: 'bg-amber-500',
68+
}
69+
return map[dot] || 'bg-zinc-500'
70+
}
71+
72+
/**
73+
* Health indicator color based on error rate.
74+
* Used by: monitoring
75+
*/
76+
export function healthColor(errorRateValue: number): string {
77+
if (errorRateValue > 5) return 'bg-red-500'
78+
if (errorRateValue > 1) return 'bg-amber-500'
79+
return 'bg-emerald-500'
80+
}
81+
82+
/**
83+
* Queue status display label.
84+
* Used by: queues
85+
*/
86+
export function statusLabel(status: string): string {
87+
const map: Record<string, string> = {
88+
active: 'Active',
89+
paused: 'Paused',
90+
stopped: 'Stopped',
91+
}
92+
return map[status] || status
93+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Browser entry point — assigns all shared functions to globalThis
3+
* so they're accessible in stx template expressions.
4+
*/
5+
import {
6+
badgeClass,
7+
chartOptions,
8+
createLineDataset,
9+
DEFAULT_STATUS_COLOR,
10+
dotColor,
11+
formatDuration,
12+
formatEventTime,
13+
formatTime,
14+
healthColor,
15+
MAX_EVENTS,
16+
progressBarColorClass,
17+
queueBadgeClass,
18+
REFRESH_INTERVAL,
19+
statusLabel,
20+
STATUS_COLORS,
21+
} from './index'
22+
23+
Object.assign(globalThis, {
24+
badgeClass,
25+
chartOptions,
26+
createLineDataset,
27+
DEFAULT_STATUS_COLOR,
28+
dotColor,
29+
formatDuration,
30+
formatEventTime,
31+
formatTime,
32+
healthColor,
33+
MAX_EVENTS,
34+
progressBarColorClass,
35+
queueBadgeClass,
36+
REFRESH_INTERVAL,
37+
statusLabel,
38+
STATUS_COLORS,
39+
})
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Shared Chart.js configuration for the devtools dashboard dark theme.
3+
*/
4+
5+
/**
6+
* Default chart options for the dark-themed dashboard.
7+
* Used by: metrics, index, queue-details
8+
*/
9+
export function chartOptions(overrides?: Record<string, unknown>): Record<string, unknown> {
10+
const defaults: Record<string, unknown> = {
11+
responsive: true,
12+
maintainAspectRatio: false,
13+
plugins: { legend: { display: false } },
14+
scales: {
15+
x: {
16+
grid: { color: 'rgba(39,39,42,0.5)' },
17+
ticks: { color: '#71717a', font: { size: 10 }, maxRotation: 0, maxTicksLimit: 10 },
18+
},
19+
y: {
20+
grid: { color: 'rgba(39,39,42,0.5)' },
21+
ticks: { color: '#71717a', font: { size: 10 } },
22+
beginAtZero: true,
23+
},
24+
},
25+
interaction: { intersect: false, mode: 'index' },
26+
}
27+
28+
if (!overrides) return defaults
29+
30+
return { ...defaults, ...overrides }
31+
}
32+
33+
interface LineDataset {
34+
label: string
35+
data: number[]
36+
borderColor: string
37+
backgroundColor: string
38+
fill: boolean
39+
tension: number
40+
pointRadius: number
41+
pointHitRadius: number
42+
borderWidth: number
43+
}
44+
45+
/**
46+
* Create a styled line dataset for Chart.js.
47+
* Used by: metrics
48+
*/
49+
export function createLineDataset(label: string, data: number[], color: string): LineDataset {
50+
return {
51+
label,
52+
data,
53+
borderColor: color,
54+
backgroundColor: color.replace(')', ', 0.1)').replace('rgb', 'rgba'),
55+
fill: true,
56+
tension: 0.4,
57+
pointRadius: 0,
58+
pointHitRadius: 10,
59+
borderWidth: 2,
60+
}
61+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Shared constants for the devtools dashboard.
3+
*/
4+
5+
/** Status colors for D3 dependency graph nodes */
6+
export const STATUS_COLORS: Record<string, string> = {
7+
completed: '#10b981',
8+
active: '#6366f1',
9+
waiting: '#f59e0b',
10+
pending: '#f59e0b',
11+
failed: '#ef4444',
12+
}
13+
14+
/** Default color for unknown statuses */
15+
export const DEFAULT_STATUS_COLOR = '#71717a'
16+
17+
/** Monitoring auto-refresh interval in milliseconds */
18+
export const REFRESH_INTERVAL = 3000
19+
20+
/** Maximum number of events to keep in the monitoring event log */
21+
export const MAX_EVENTS = 50
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Shared formatting utilities for the devtools dashboard.
3+
*/
4+
5+
/**
6+
* Format an ISO timestamp to a locale string.
7+
* Returns em-dash for null/undefined/empty values.
8+
* Used by: batches, batch-details, queue-details, job-details, group-details
9+
*/
10+
export function formatTime(ts?: string): string {
11+
if (!ts) return '\u2014'
12+
return new Date(ts).toLocaleString()
13+
}
14+
15+
/**
16+
* Format a duration in milliseconds to a human-readable string.
17+
* Used by: index (dashboard overview)
18+
*/
19+
export function formatDuration(duration?: number): string {
20+
return duration ? (duration / 1000).toFixed(1) + 's' : '\u2014'
21+
}
22+
23+
/**
24+
* Format the current time as HH:MM:SS for event log entries.
25+
* Used by: monitoring
26+
*/
27+
export function formatEventTime(): string {
28+
return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
29+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Barrel export for all shared devtools functions.
3+
* This file is the entrypoint for Bun.build to create the browser bundle.
4+
*/
5+
6+
export {
7+
badgeClass,
8+
dotColor,
9+
healthColor,
10+
progressBarColorClass,
11+
queueBadgeClass,
12+
statusLabel,
13+
} from './badges'
14+
15+
export {
16+
formatDuration,
17+
formatEventTime,
18+
formatTime,
19+
} from './formatters'
20+
21+
export {
22+
chartOptions,
23+
createLineDataset,
24+
} from './chart'
25+
26+
export {
27+
DEFAULT_STATUS_COLOR,
28+
MAX_EVENTS,
29+
REFRESH_INTERVAL,
30+
STATUS_COLORS,
31+
} from './constants'

packages/devtools/src/index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,29 @@ export { JobStatus } from './types'
1010
export { createApiRoutes, fetchBatches, fetchDashboardStats, fetchDependencyGraph, fetchJobGroups, fetchJobs, fetchMetrics, fetchQueueMetrics, fetchQueues } from './api'
1111

1212
const PAGES_DIR = path.join(import.meta.dir, 'pages')
13+
const FUNCTIONS_ENTRY = path.join(import.meta.dir, 'functions', 'browser.ts')
1314

1415
let broadcastServer: BroadcastServer | null = null
16+
let bundledFunctionsJs: string | null = null
17+
18+
async function buildFunctionsBundle(): Promise<string> {
19+
if (bundledFunctionsJs) return bundledFunctionsJs
20+
21+
const result = await Bun.build({
22+
entrypoints: [FUNCTIONS_ENTRY],
23+
target: 'browser',
24+
minify: false,
25+
format: 'iife',
26+
})
27+
28+
if (!result.success) {
29+
console.error('Failed to build functions bundle:', result.logs)
30+
return ''
31+
}
32+
33+
bundledFunctionsJs = await result.outputs[0].text()
34+
return bundledFunctionsJs
35+
}
1536

1637
async function renderStxPage(templateName: string, wsUrl: string): Promise<string> {
1738
const templatePath = path.join(PAGES_DIR, `${templateName}.stx`)
@@ -242,6 +263,12 @@ catch { /* try next queue */ }
242263
return Response.json(allJobs.slice(0, batch.totalJobs > 10 ? 10 : batch.totalJobs))
243264
}
244265

266+
// Shared functions bundle
267+
if (path === '/bq-utils.js') {
268+
const js = await buildFunctionsBundle()
269+
return new Response(js, { headers: { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-store' } })
270+
}
271+
245272
// Static page routes
246273
const pageMap: Record<string, string> = {
247274
'/': 'index',

0 commit comments

Comments
 (0)