11import type { DashboardConfig } from './types'
22import path from 'node:path'
3- import { defaultConfig as stxDefaultConfig , isSpaNavigation , processDirectives , stripDocumentWrapper } from '@stacksjs/stx'
3+ import { serveApp } from '@stacksjs/stx'
44import { BroadcastServer } from 'ts-broadcasting'
5- import { createApiRoutes , fetchBatchById , fetchJobById , fetchJobGroups , fetchJobs , fetchQueueById } from './api'
65import { resolveConfig } from './api'
76
87export type { Batch , DashboardConfig , DashboardStats , DependencyGraph , DependencyNode , JobData , JobGroup , MetricsData , Queue , QueueMetrics } from './types'
98export { JobStatus } from './types'
109export { createApiRoutes , fetchBatches , fetchDashboardStats , fetchDependencyGraph , fetchJobGroups , fetchJobs , fetchMetrics , fetchQueueMetrics , fetchQueues } from './api'
1110
12- const SRC_DIR = import . meta. dir
13- const PAGES_DIR = path . join ( SRC_DIR , 'pages' )
14- const FUNCTIONS_ENTRY = path . join ( SRC_DIR , 'functions' , 'browser.ts' )
15-
1611let broadcastServer : BroadcastServer | null = null
17- let bundledFunctionsJs : string | null = null
18-
19- const stxConfig = {
20- ...stxDefaultConfig ,
21- componentsDir : path . join ( SRC_DIR , 'components' ) ,
22- layoutsDir : path . join ( SRC_DIR , 'layouts' ) ,
23- partialsDir : path . join ( SRC_DIR , 'partials' ) ,
24- }
25-
26- async function buildFunctionsBundle ( ) : Promise < string > {
27- if ( bundledFunctionsJs ) return bundledFunctionsJs
28- const result = await Bun . build ( { entrypoints : [ FUNCTIONS_ENTRY ] , target : 'browser' , minify : false , format : 'iife' } )
29- if ( ! result . success ) { console . error ( 'Failed to build functions bundle:' , result . logs ) ; return '' } // eslint-disable-line no-console
30- bundledFunctionsJs = await result . outputs [ 0 ] . text ( )
31- return bundledFunctionsJs
32- }
33-
34- async function renderPage ( templateName : string , wsUrl : string , req : Request ) : Promise < Response > {
35- const templatePath = path . join ( PAGES_DIR , `${ templateName } .stx` )
36- const content = await Bun . file ( templatePath ) . text ( )
37- const context : Record < string , any > = { __filename : templatePath , __dirname : path . dirname ( templatePath ) }
38-
39- // SPA navigation — process page as fragment (strip layout directives)
40- if ( isSpaNavigation ( req ) ) {
41- const pageContent = content
42- . replace ( / @ e x t e n d s \s * \( \s * [ ' " ] [ ^ ' " ] + [ ' " ] \s * \) / g, '' )
43- . replace ( / @ s e c t i o n \s * \( \s * ' t i t l e ' \s * \) [ ^ @ ] * @ e n d s e c t i o n / g, '' )
44- . replace ( / @ s e c t i o n \s * \( \s * ' c o n t e n t ' \s * \) / g, '' )
45- . replace ( / @ e n d s e c t i o n \s * $ / gm, '' )
46- . replace ( / @ p u s h \s * \( \s * [ ' " ] [ ^ ' " ] + [ ' " ] \s * \) / g, '' )
47- . replace ( / @ e n d p u s h / g, '' )
48-
49- let fragment = await processDirectives ( pageContent , context , templatePath , stxConfig , new Set ( ) )
50- fragment = stripDocumentWrapper ( fragment )
51- // Strip signals runtime — shell already has it
52- fragment = fragment . replace ( / < s c r i p t d a t a - s t x - s c o p e d > \( f u n c t i o n \( \) \{ [ ^ ] * ?< \/ s c r i p t > / , '' )
53-
54- return new Response ( fragment , {
55- headers : { 'Content-Type' : 'text/html' , 'Cache-Control' : 'no-store' , 'X-STX-Fragment' : 'true' } ,
56- } )
57- }
58-
59- // Full page — processDirectives handles @extends, layout, signals runtime, everything
60- let html = await processDirectives ( content , context , templatePath , stxConfig , new Set ( ) )
61- html = html . replace ( '</head>' , `<script>window.__BQ_WS_URL = "${ wsUrl } ";</script>\n</head>` )
62- return new Response ( html , { headers : { 'Content-Type' : 'text/html' , 'Cache-Control' : 'no-store' } } )
63- }
6412
6513function wireQueueEvents ( queues : any [ ] ) : void {
6614 if ( ! broadcastServer ) return
@@ -78,96 +26,22 @@ function wireQueueEvents(queues: any[]): void {
7826 }
7927}
8028
81- function handleDynamicApiRoute ( pathname : string , req : Request , config : DashboardConfig ) : Promise < Response > | null {
82- const queueMatch = pathname . match ( / ^ \/ a p i \/ q u e u e s \/ ( [ ^ / ] + ) $ / )
83- if ( queueMatch ) return fetchQueueById ( config , queueMatch [ 1 ] ) . then ( q => q ? Response . json ( q ) : Response . json ( { error : 'Queue not found' } , { status : 404 } ) )
84-
85- const retryMatch = pathname . match ( / ^ \/ a p i \/ j o b s \/ ( [ ^ / ] + ) \/ r e t r y $ / )
86- if ( retryMatch && req . method === 'POST' ) {
87- const jobId = decodeURIComponent ( retryMatch [ 1 ] )
88- const qs = config . queues ?. length ? config . queues : config . queueManager ? ( ( ) => { const arr : any [ ] = [ ] ; for ( const c of config . queueManager ! . getConnections ( ) ) { try { const conn = config . queueManager ! . connection ( c ) ; for ( const q of conn . queues . values ( ) ) arr . push ( q ) } catch { } } return arr } ) ( ) : [ ]
89- return ( async ( ) => { for ( const q of qs ) { try { const r = await q . retryJob ( jobId ) ; if ( r ) return Response . json ( { success : true } ) } catch { } } return Response . json ( { success : false } ) } ) ( )
90- }
91-
92- const deleteMatch = pathname . match ( / ^ \/ a p i \/ j o b s \/ ( [ ^ / ] + ) $ / )
93- if ( deleteMatch && req . method === 'DELETE' ) {
94- const jobId = decodeURIComponent ( deleteMatch [ 1 ] )
95- const qs = config . queues ?. length ? config . queues : config . queueManager ? ( ( ) => { const arr : any [ ] = [ ] ; for ( const c of config . queueManager ! . getConnections ( ) ) { try { const conn = config . queueManager ! . connection ( c ) ; for ( const q of conn . queues . values ( ) ) arr . push ( q ) } catch { } } return arr } ) ( ) : [ ]
96- return ( async ( ) => { for ( const q of qs ) { try { await q . removeJob ( jobId ) ; return Response . json ( { success : true } ) } catch { } } return Response . json ( { success : false } ) } ) ( )
97- }
98-
99- const jobMatch = pathname . match ( / ^ \/ a p i \/ j o b s \/ ( [ ^ / ] + ) $ / )
100- if ( jobMatch && req . method === 'GET' ) return fetchJobById ( config , jobMatch [ 1 ] ) . then ( j => j ? Response . json ( j ) : Response . json ( { error : 'Job not found' } , { status : 404 } ) )
101-
102- const groupMatch = pathname . match ( / ^ \/ a p i \/ g r o u p s \/ ( [ ^ / ] + ) $ / )
103- if ( groupMatch ) return fetchJobGroups ( config ) . then ( groups => { const g = groups . find ( x => x . id === groupMatch [ 1 ] ) ; return g ? Response . json ( g ) : Response . json ( { error : 'Group not found' } , { status : 404 } ) } )
104-
105- const groupJobsMatch = pathname . match ( / ^ \/ a p i \/ g r o u p s \/ ( [ ^ / ] + ) \/ j o b s $ / )
106- if ( groupJobsMatch ) return fetchJobGroups ( config ) . then ( async ( groups ) => { const g = groups . find ( x => x . id === groupJobsMatch [ 1 ] ) ; if ( ! g ) return Response . json ( { error : 'Group not found' } , { status : 404 } ) ; const allJobs = await fetchJobs ( config ) ; return Response . json ( allJobs . filter ( j => j . name . toLowerCase ( ) . includes ( g . name . toLowerCase ( ) . split ( ' ' ) [ 0 ] ) ) ) } )
107-
108- const batchMatch = pathname . match ( / ^ \/ a p i \/ b a t c h e s \/ ( [ ^ / ] + ) $ / )
109- if ( batchMatch ) return fetchBatchById ( config , batchMatch [ 1 ] ) . then ( b => b ? Response . json ( b ) : Response . json ( { error : 'Batch not found' } , { status : 404 } ) )
110-
111- const batchJobsMatch = pathname . match ( / ^ \/ a p i \/ b a t c h e s \/ ( [ ^ / ] + ) \/ j o b s $ / )
112- if ( batchJobsMatch ) return fetchBatchById ( config , batchJobsMatch [ 1 ] ) . then ( async ( b ) => { if ( ! b ) return Response . json ( { error : 'Batch not found' } , { status : 404 } ) ; const allJobs = await fetchJobs ( config ) ; return Response . json ( allJobs . slice ( 0 , b . totalJobs > 10 ? 10 : b . totalJobs ) ) } )
113-
114- return null
115- }
116-
11729export async function serveDashboard ( options : DashboardConfig = { } ) : Promise < void > {
11830 const config = resolveConfig ( options )
119- const apiRoutes = createApiRoutes ( config )
12031
32+ // Start WebSocket broadcast server
12133 const broadcastPort = options . broadcastPort || 6001
12234 broadcastServer = new BroadcastServer ( { connections : { default : { driver : 'bun' , host : '0.0.0.0' , port : broadcastPort } } , default : 'default' , debug : false } )
12335 await broadcastServer . start ( )
12436
12537 const allQueues = config . queues || [ ]
12638 if ( allQueues . length ) wireQueueEvents ( allQueues )
12739
128- const wsUrl = `ws://localhost:${ broadcastPort } /app`
129-
130- const pageMap : Record < string , string > = {
131- '/' : 'index' , '/monitoring' : 'monitoring' , '/metrics' : 'metrics' , '/queues' : 'queues' ,
132- '/jobs' : 'jobs' , '/batches' : 'batches' , '/groups' : 'groups' , '/dependencies' : 'dependencies' ,
133- }
134- const dynamicPages = [
135- { pattern : / ^ \/ q u e u e s \/ [ ^ / ] + $ / , template : 'queue-details' } ,
136- { pattern : / ^ \/ j o b s \/ [ ^ / ] + $ / , template : 'job-details' } ,
137- { pattern : / ^ \/ b a t c h e s \/ [ ^ / ] + $ / , template : 'batch-details' } ,
138- { pattern : / ^ \/ g r o u p s \/ [ ^ / ] + $ / , template : 'group-details' } ,
139- ]
140-
141- Bun . serve ( {
40+ // Let stx handle everything — pages, layout, SPA fragments, routing
41+ const appDir = path . join ( import . meta. dir )
42+ await serveApp ( appDir , {
14243 port : config . port ,
143- hostname : config . host ,
144- async fetch ( req : Request ) {
145- const pathname = new URL ( req . url ) . pathname
146-
147- // Static API routes
148- const apiHandler = apiRoutes [ pathname as keyof typeof apiRoutes ]
149- if ( apiHandler ) return apiHandler ( req )
150-
151- // Dynamic API routes
152- const dynamicApi = handleDynamicApiRoute ( pathname , req , config )
153- if ( dynamicApi ) return dynamicApi
154-
155- // Shared functions bundle
156- if ( pathname === '/bq-utils.js' ) {
157- const js = await buildFunctionsBundle ( )
158- return new Response ( js , { headers : { 'Content-Type' : 'application/javascript' , 'Cache-Control' : 'no-store' } } )
159- }
160-
161- // Page routes
162- if ( pageMap [ pathname ] ) return renderPage ( pageMap [ pathname ] , wsUrl , req )
163- for ( const { pattern, template } of dynamicPages ) {
164- if ( pattern . test ( pathname ) ) return renderPage ( template , wsUrl , req )
165- }
166-
167- return new Response ( 'Not Found' , { status : 404 } )
168- } ,
44+ watch : true ,
45+ hotReload : false ,
16946 } )
170-
171- console . log ( `bun-queue dashboard running at http://localhost:${ config . port } ` ) // eslint-disable-line no-console
172- console . log ( `WebSocket broadcast server running at ws://localhost:${ broadcastPort } /app` ) // eslint-disable-line no-console
17347}
0 commit comments