2025-11-29 19:26:20 -05:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
2025-11-30 13:03:18 -05:00
< title > PULSE - Workflow Orchestration< / title >
2025-11-29 19:26:20 -05:00
< style >
2026-01-07 20:12:16 -05:00
: root {
/* Terminal Dark Backgrounds */
--bg-primary : #0a0a0a ;
--bg-secondary : #1a1a1a ;
--bg-tertiary : #2a2a2a ;
/* Terminal Colors */
--terminal-green : #00ff41 ;
--terminal-amber : #ffb000 ;
--terminal-cyan : #00ffff ;
--text-primary : #00ff41 ;
--text-secondary : #00cc33 ;
--text-muted : #008822 ;
/* Border & UI */
--border-color : #00ff41 ;
--shadow : none ;
--hover-bg : rgba ( 0 , 255 , 65 , 0.1 ) ;
/* Status Colors (adapted) */
--status-online : #28a745 ;
--status-offline : #dc3545 ;
--status-running : #ffc107 ;
--status-completed : #28a745 ;
--status-failed : #dc3545 ;
--status-waiting : #ffc107 ;
/* Terminal Font Stack */
--font-mono : 'Courier New' , 'Consolas' , 'Monaco' , 'Menlo' , monospace ;
/* Glow Effects */
--glow-green : 0 0 5 px #00ff41 , 0 0 10 px #00ff41 , 0 0 15 px #00ff41 ;
--glow-green-intense : 0 0 8 px #00ff41 , 0 0 16 px #00ff41 , 0 0 24 px #00ff41 , 0 0 32 px rgba ( 0 , 255 , 65 , 0.5 ) ;
--glow-amber : 0 0 5 px #ffb000 , 0 0 10 px #ffb000 , 0 0 15 px #ffb000 ;
--glow-amber-intense : 0 0 8 px #ffb000 , 0 0 16 px #ffb000 , 0 0 24 px #ffb000 ;
}
2025-11-29 19:26:20 -05:00
* { margin : 0 ; padding : 0 ; box-sizing : border-box ; }
2026-01-07 20:12:16 -05:00
2025-11-29 19:26:20 -05:00
body {
2026-01-07 20:12:16 -05:00
font-family : var ( - - font - mono ) ;
background : var ( - - bg - primary ) ;
color : var ( - - text - primary ) ;
2025-11-29 19:26:20 -05:00
min-height : 100 vh ;
padding : 20 px ;
2026-01-07 20:12:16 -05:00
position : relative ;
animation : flicker 0.2 s ease-in-out 30 s infinite ;
}
/* CRT Scanline Effect */
body :: before {
content : '' ;
position : fixed ;
top : 0 ;
left : 0 ;
width : 100 % ;
height : 100 % ;
background : repeating-linear-gradient (
0 deg ,
rgba ( 0 , 0 , 0 , 0.15 ) ,
rgba ( 0 , 0 , 0 , 0.15 ) 1 px ,
transparent 1 px ,
transparent 2 px
) ;
pointer-events : none ;
z-index : 9999 ;
animation : scanline 8 s linear infinite ;
}
@ keyframes scanline {
0 % { transform : translateY ( 0 ) ; }
100 % { transform : translateY ( 4 px ) ; }
}
/* Data Stream Corner Effect */
body :: after {
content : '10101010' ;
position : fixed ;
bottom : 10 px ;
right : 10 px ;
font-family : var ( - - font - mono ) ;
font-size : 0.6 rem ;
color : var ( - - terminal - green ) ;
opacity : 0.1 ;
pointer-events : none ;
letter-spacing : 2 px ;
animation : data-stream 3 s linear infinite ;
}
@ keyframes data-stream {
0 % { content : '10101010' ; opacity : 0.1 ; }
25 % { content : '01010101' ; opacity : 0.15 ; }
50 % { content : '11001100' ; opacity : 0.1 ; }
75 % { content : '00110011' ; opacity : 0.15 ; }
100 % { content : '10101010' ; opacity : 0.1 ; }
}
@ keyframes flicker {
0 % { opacity : 1 ; }
10 % { opacity : 0.95 ; }
20 % { opacity : 1 ; }
30 % { opacity : 0.97 ; }
40 % { opacity : 1 ; }
2025-11-29 19:26:20 -05:00
}
2025-11-30 13:03:18 -05:00
. container { max-width : 1600 px ; margin : 0 auto ; }
2025-11-29 19:26:20 -05:00
. header {
2026-01-07 20:12:16 -05:00
background : var ( - - bg - secondary ) ;
padding : 20 px 30 px ;
border : 2 px solid var ( - - terminal - green ) ;
border-radius : 0 ;
box-shadow : none ;
2025-11-29 19:26:20 -05:00
margin-bottom : 30 px ;
display : flex ;
justify-content : space-between ;
align-items : center ;
2026-01-07 20:12:16 -05:00
position : relative ;
}
. header :: before {
content : '╔' ;
position : absolute ;
top : -2 px ;
left : -2 px ;
font-size : 1.5 rem ;
color : var ( - - terminal - green ) ;
text-shadow : var ( - - glow - green ) ;
line-height : 1 ;
z-index : 10 ;
}
. header :: after {
content : '╗' ;
position : absolute ;
top : -2 px ;
right : -2 px ;
font-size : 1.5 rem ;
color : var ( - - terminal - green ) ;
text-shadow : var ( - - glow - green ) ;
line-height : 1 ;
z-index : 10 ;
}
. header-left h1 {
color : var ( - - terminal - amber ) ;
font-size : 2 em ;
margin-bottom : 5 px ;
text-shadow : var ( - - glow - amber - intense ) ;
font-family : var ( - - font - mono ) ;
}
. header-left h1 :: before {
content : '>> ' ;
color : var ( - - terminal - green ) ;
}
. header-left p {
color : var ( - - terminal - green ) ;
font-size : 1 em ;
font-family : var ( - - font - mono ) ;
2025-11-29 19:26:20 -05:00
}
2025-11-30 13:03:18 -05:00
. user-info { text-align : right ; }
2026-01-07 20:12:16 -05:00
. user-info . name {
font-weight : 600 ;
color : var ( - - terminal - green ) ;
font-size : 1.1 em ;
font-family : var ( - - font - mono ) ;
}
. user-info . email {
color : var ( - - text - secondary ) ;
font-size : 0.9 em ;
font-family : var ( - - font - mono ) ;
}
2025-11-29 19:26:20 -05:00
. user-info . badge {
display : inline-block ;
2026-01-07 20:12:16 -05:00
background : transparent ;
color : var ( - - terminal - amber ) ;
2025-11-29 19:26:20 -05:00
padding : 4 px 12 px ;
2026-01-07 20:12:16 -05:00
border : 2 px solid var ( - - terminal - amber ) ;
border-radius : 0 ;
2025-11-29 19:26:20 -05:00
font-size : 0.8 em ;
margin-top : 5 px ;
margin-left : 5 px ;
2026-01-07 20:12:16 -05:00
font-family : var ( - - font - mono ) ;
}
. user-info . badge :: before {
content : '[' ;
margin-right : 3 px ;
}
. user-info . badge :: after {
content : ']' ;
margin-left : 3 px ;
2025-11-29 19:26:20 -05:00
}
2025-11-30 13:03:18 -05:00
. tabs {
2026-01-07 20:12:16 -05:00
background : var ( - - bg - secondary ) ;
border : 2 px solid var ( - - terminal - green ) ;
border-radius : 0 ;
2025-11-30 13:03:18 -05:00
padding : 10 px ;
margin-bottom : 20 px ;
display : flex ;
gap : 10 px ;
}
. tab {
padding : 12 px 24 px ;
background : transparent ;
2026-01-07 20:12:16 -05:00
border : 2 px solid var ( - - terminal - green ) ;
border-radius : 0 ;
2025-11-30 13:03:18 -05:00
cursor : pointer ;
font-size : 1 em ;
font-weight : 600 ;
2026-01-07 20:12:16 -05:00
color : var ( - - terminal - green ) ;
font-family : var ( - - font - mono ) ;
2025-11-30 13:03:18 -05:00
transition : all 0.3 s ;
}
. tab . active {
2026-01-07 20:12:16 -05:00
background : rgba ( 0 , 255 , 65 , 0.2 ) ;
color : var ( - - terminal - amber ) ;
border-color : var ( - - terminal - amber ) ;
text-shadow : var ( - - glow - amber ) ;
}
. tab : hover {
background : rgba ( 0 , 255 , 65 , 0.1 ) ;
color : var ( - - terminal - amber ) ;
}
. tab . active : hover {
background : rgba ( 0 , 255 , 65 , 0.25 ) ;
2025-11-30 13:03:18 -05:00
}
2025-11-29 19:26:20 -05:00
. grid {
display : grid ;
2025-11-30 13:03:18 -05:00
grid-template-columns : repeat ( auto - fit , minmax ( 400 px , 1 fr ) ) ;
2025-11-29 19:26:20 -05:00
gap : 20 px ;
margin-bottom : 30 px ;
}
. card {
2026-01-07 20:12:16 -05:00
background : var ( - - bg - secondary ) ;
2025-11-29 19:26:20 -05:00
padding : 25 px ;
2026-01-07 20:12:16 -05:00
border : 2 px solid var ( - - terminal - green ) ;
border-radius : 0 ;
box-shadow : none ;
position : relative ;
}
. card :: before {
content : '┌' ;
position : absolute ;
top : -2 px ;
left : -2 px ;
font-size : 1.2 rem ;
color : var ( - - terminal - green ) ;
line-height : 1 ;
}
. card :: after {
content : '┐' ;
position : absolute ;
top : -2 px ;
right : -2 px ;
font-size : 1.2 rem ;
color : var ( - - terminal - green ) ;
line-height : 1 ;
}
. card h3 {
color : var ( - - terminal - amber ) ;
margin-bottom : 15 px ;
font-size : 1.2 em ;
font-family : var ( - - font - mono ) ;
text-shadow : var ( - - glow - amber ) ;
}
. card h3 :: before {
content : '═══ ' ;
color : var ( - - terminal - green ) ;
}
. card h3 :: after {
content : ' ═══' ;
color : var ( - - terminal - green ) ;
2025-11-29 19:26:20 -05:00
}
. status {
display : inline-block ;
padding : 5 px 15 px ;
2026-01-07 20:12:16 -05:00
border-radius : 0 ;
2025-11-29 19:26:20 -05:00
font-size : 0.9 em ;
font-weight : 600 ;
margin-bottom : 5 px ;
2026-01-07 20:12:16 -05:00
background : transparent ;
border : 2 px solid ;
font-family : var ( - - font - mono ) ;
}
. status . online {
border-color : var ( - - status - online ) ;
color : var ( - - status - online ) ;
text-shadow : 0 0 5 px var ( - - status - online ) , 0 0 10 px var ( - - status - online ) ;
}
. status . online :: before { content : '[●' ; margin-right : 4 px ; }
. status . online :: after { content : ']' ; margin-left : 4 px ; }
. status . offline {
border-color : var ( - - status - offline ) ;
color : var ( - - status - offline ) ;
text-shadow : 0 0 5 px var ( - - status - offline ) , 0 0 10 px var ( - - status - offline ) ;
}
. status . offline :: before { content : '[○' ; margin-right : 4 px ; }
. status . offline :: after { content : ']' ; margin-left : 4 px ; }
. status . running {
border-color : var ( - - status - running ) ;
color : var ( - - status - running ) ;
text-shadow : 0 0 5 px var ( - - status - running ) , 0 0 10 px var ( - - status - running ) ;
}
. status . running :: before {
content : '[◐' ;
margin-right : 4 px ;
animation : spin-status 2 s linear infinite ;
}
@ keyframes spin-status {
0 % { content : '[◐' ; }
25 % { content : '[◓' ; }
50 % { content : '[◑' ; }
75 % { content : '[◒' ; }
100 % { content : '[◐' ; }
}
. status . running :: after { content : ']' ; margin-left : 4 px ; }
. status . completed {
border-color : var ( - - status - completed ) ;
color : var ( - - status - completed ) ;
text-shadow : 0 0 5 px var ( - - status - completed ) , 0 0 10 px var ( - - status - completed ) ;
2025-11-29 19:26:20 -05:00
}
2026-01-07 20:12:16 -05:00
. status . completed :: before { content : '[✓' ; margin-right : 4 px ; }
. status . completed :: after { content : ']' ; margin-left : 4 px ; }
. status . failed {
border-color : var ( - - status - failed ) ;
color : var ( - - status - failed ) ;
text-shadow : 0 0 5 px var ( - - status - failed ) , 0 0 10 px var ( - - status - failed ) ;
}
. status . failed :: before { content : '[✗' ; margin-right : 4 px ; }
. status . failed :: after { content : ']' ; margin-left : 4 px ; }
. status . waiting {
border-color : var ( - - status - waiting ) ;
color : var ( - - status - waiting ) ;
text-shadow : 0 0 5 px var ( - - status - waiting ) , 0 0 10 px var ( - - status - waiting ) ;
}
. status . waiting :: before { content : '[⏳' ; margin-right : 4 px ; }
. status . waiting :: after { content : ']' ; margin-left : 4 px ; }
2025-11-29 19:26:20 -05:00
button {
2026-01-07 20:12:16 -05:00
background : transparent ;
color : var ( - - terminal - green ) ;
border : 2 px solid var ( - - terminal - green ) ;
border-radius : 0 ;
2025-11-29 19:26:20 -05:00
padding : 12 px 24 px ;
cursor : pointer ;
font-size : 1 em ;
font-weight : 600 ;
2026-01-07 20:12:16 -05:00
font-family : var ( - - font - mono ) ;
text-transform : uppercase ;
transition : all 0.3 s ease ;
2025-11-29 19:26:20 -05:00
margin-right : 10 px ;
margin-bottom : 10 px ;
}
2026-01-07 20:12:16 -05:00
button :: before { content : '[ ' ; }
button :: after { content : ' ]' ; }
2025-11-29 19:26:20 -05:00
button : hover {
2026-01-07 20:12:16 -05:00
background : rgba ( 0 , 255 , 65 , 0.15 ) ;
color : var ( - - terminal - amber ) ;
border-color : var ( - - terminal - amber ) ;
text-shadow : var ( - - glow - amber ) ;
box-shadow : var ( - - glow - amber ) ;
2025-11-29 19:26:20 -05:00
transform : translateY ( -2 px ) ;
}
2026-01-07 20:12:16 -05:00
button . danger {
color : var ( - - status - failed ) ;
border-color : var ( - - status - failed ) ;
}
button . danger : hover {
background : rgba ( 220 , 53 , 69 , 0.15 ) ;
text-shadow : 0 0 5 px var ( - - status - failed ) , 0 0 10 px var ( - - status - failed ) ;
}
2025-11-30 13:03:18 -05:00
button . small {
padding : 6 px 12 px ;
font-size : 0.85 em ;
2025-11-29 19:26:20 -05:00
}
2025-11-30 13:03:18 -05:00
. worker-item , . execution-item , . workflow-item {
2025-11-29 19:26:20 -05:00
padding : 15 px ;
2026-01-07 20:12:16 -05:00
border : 2 px solid var ( - - terminal - green ) ;
border-radius : 0 ;
2025-11-29 19:26:20 -05:00
margin-bottom : 10 px ;
2026-01-07 20:12:16 -05:00
background : var ( - - bg - secondary ) ;
font-family : var ( - - font - mono ) ;
transition : all 0.3 s ;
2025-11-29 19:26:20 -05:00
}
2025-11-30 13:03:18 -05:00
. worker-item : hover , . execution-item : hover , . workflow-item : hover {
2026-01-07 20:12:16 -05:00
background : rgba ( 0 , 255 , 65 , 0.08 ) ;
box-shadow : inset 0 0 20 px rgba ( 0 , 255 , 65 , 0.1 ) ;
}
. workflow-name {
font-weight : 600 ;
color : var ( - - terminal - amber ) ;
font-size : 1.1 em ;
margin-bottom : 5 px ;
font-family : var ( - - font - mono ) ;
text-shadow : var ( - - glow - amber ) ;
}
. workflow-name :: before {
content : '> ' ;
color : var ( - - terminal - green ) ;
}
. workflow-desc {
color : var ( - - terminal - green ) ;
font-size : 0.9 em ;
margin-bottom : 10 px ;
font-family : var ( - - font - mono ) ;
}
. loading {
text-align : center ;
padding : 20 px ;
color : var ( - - terminal - green ) ;
font-family : var ( - - font - mono ) ;
}
. loading :: after {
content : '...' ;
animation : loading-dots 1.5 s steps ( 4 , end ) infinite ;
}
@ keyframes loading-dots {
0 % , 20 % { content : '.' ; }
40 % { content : '..' ; }
60 % , 100 % { content : '...' ; }
}
. empty {
text-align : center ;
padding : 30 px ;
color : var ( - - text - muted ) ;
font-family : var ( - - font - mono ) ;
}
. empty :: before {
content : '[ NO DATA ]' ;
display : block ;
font-size : 1.2 rem ;
color : var ( - - terminal - green ) ;
margin-bottom : 10 px ;
}
. timestamp {
font-size : 0.85 em ;
color : var ( - - text - muted ) ;
font-family : var ( - - font - mono ) ;
2025-11-29 19:26:20 -05:00
}
2025-11-30 13:03:18 -05:00
. modal {
display : none ;
position : fixed ;
top : 0 ;
left : 0 ;
width : 100 % ;
height : 100 % ;
2026-01-07 20:12:16 -05:00
background : rgba ( 0 , 0 , 0 , 0.85 ) ;
2025-11-30 13:03:18 -05:00
z-index : 1000 ;
align-items : center ;
justify-content : center ;
2025-11-29 19:26:20 -05:00
}
2025-11-30 13:03:18 -05:00
. modal . show { display : flex ; }
. modal-content {
2026-01-07 20:12:16 -05:00
background : var ( - - bg - primary ) ;
padding : 0 ;
border : 3 px double var ( - - terminal - green ) ;
border-radius : 0 ;
2025-11-30 13:03:18 -05:00
max-width : 600 px ;
width : 90 % ;
max-height : 80 vh ;
overflow-y : auto ;
2026-01-07 20:12:16 -05:00
box-shadow : 0 0 30 px rgba ( 0 , 255 , 65 , 0.3 ) ;
position : relative ;
}
. modal-content :: before {
content : '╔' ;
position : absolute ;
top : -3 px ;
left : -3 px ;
font-size : 1.5 rem ;
color : var ( - - terminal - green ) ;
line-height : 1 ;
z-index : 10 ;
}
. modal-content :: after {
content : '╗' ;
position : absolute ;
top : -3 px ;
right : -3 px ;
font-size : 1.5 rem ;
color : var ( - - terminal - green ) ;
line-height : 1 ;
z-index : 10 ;
}
. modal-content h2 {
margin : 0 ;
padding : 20 px 30 px ;
background : var ( - - bg - secondary ) ;
color : var ( - - terminal - amber ) ;
border-bottom : 2 px solid var ( - - terminal - green ) ;
font-family : var ( - - font - mono ) ;
text-shadow : var ( - - glow - amber ) ;
}
. modal-content h2 :: before {
content : '═══ ' ;
color : var ( - - terminal - green ) ;
}
. modal-content h2 :: after {
content : ' ═══' ;
color : var ( - - terminal - green ) ;
2025-11-30 13:03:18 -05:00
}
input , textarea , select {
width : 100 % ;
padding : 12 px ;
margin-bottom : 15 px ;
2026-01-07 20:12:16 -05:00
border : 2 px solid var ( - - terminal - green ) ;
border-radius : 0 ;
2025-11-30 13:03:18 -05:00
font-size : 1 em ;
2026-01-07 20:12:16 -05:00
font-family : var ( - - font - mono ) ;
background : var ( - - bg - primary ) ;
color : var ( - - terminal - green ) ;
box-shadow : inset 0 0 10 px rgba ( 0 , 0 , 0 , 0.5 ) ;
2025-11-29 19:26:20 -05:00
}
2025-11-30 13:03:18 -05:00
input : focus , textarea : focus , select : focus {
outline : none ;
2026-01-07 20:12:16 -05:00
border-color : var ( - - terminal - amber ) ;
box-shadow : var ( - - glow - amber ) , inset 0 0 10 px rgba ( 0 , 0 , 0 , 0.5 ) ;
background : rgba ( 0 , 255 , 65 , 0.05 ) ;
2025-11-29 19:26:20 -05:00
}
2026-01-07 20:12:16 -05:00
textarea { min-height : 100 px ; }
2025-11-30 13:03:18 -05:00
. tab-content { display : none ; }
. tab-content . active { display : block ; }
. log-entry {
padding : 10 px ;
2026-01-07 20:12:16 -05:00
background : var ( - - bg - secondary ) ;
border-left : 3 px solid var ( - - terminal - green ) ;
2025-11-30 13:03:18 -05:00
margin-bottom : 10 px ;
2026-01-07 20:12:16 -05:00
font-family : var ( - - font - mono ) ;
2025-11-30 13:03:18 -05:00
font-size : 0.9 em ;
2026-01-07 20:12:16 -05:00
color : var ( - - terminal - green ) ;
}
. log-entry :: before {
content : '> ' ;
color : var ( - - terminal - amber ) ;
font-weight : bold ;
2025-11-30 13:03:18 -05:00
}
. prompt-box {
2026-01-07 20:12:16 -05:00
background : rgba ( 255 , 176 , 0 , 0.1 ) ;
border : 2 px solid var ( - - terminal - amber ) ;
2025-11-29 19:26:20 -05:00
padding : 20 px ;
2026-01-07 20:12:16 -05:00
border-radius : 0 ;
2025-11-30 13:03:18 -05:00
margin : 20 px 0 ;
2025-11-29 19:26:20 -05:00
}
2026-01-07 20:12:16 -05:00
. prompt-box h3 {
color : var ( - - terminal - amber ) ;
margin-bottom : 15 px ;
font-family : var ( - - font - mono ) ;
text-shadow : var ( - - glow - amber ) ;
}
. prompt-box h3 :: before {
content : '⏳ ' ;
}
2026-03-03 16:55:02 -05:00
. prompt-box p {
color : var ( - - terminal - green ) ;
font-family : var ( - - font - mono ) ;
margin-bottom : 14 px ;
}
. prompt-opt-btn {
padding : 7 px 16 px ;
margin : 4 px 4 px 4 px 0 ;
background : rgba ( 0 , 255 , 255 , 0.08 ) ;
border : 1 px solid var ( - - terminal - cyan ) ;
color : var ( - - terminal - cyan ) ;
font-family : var ( - - font - mono ) ;
font-size : 0.88 em ;
cursor : pointer ;
transition : background 0.2 s ;
}
. prompt-opt-btn : hover {
background : rgba ( 0 , 255 , 255 , 0.2 ) ;
box-shadow : 0 0 8 px rgba ( 0 , 255 , 255 , 0.3 ) ;
}
. prompt-opt-btn . answered {
opacity : 0.45 ;
cursor : default ;
background : transparent ;
}
2026-01-07 20:12:16 -05:00
/* Boot Overlay */
. boot-overlay {
position : fixed ;
top : 0 ;
left : 0 ;
width : 100 % ;
height : 100 % ;
background : var ( - - bg - primary ) ;
z-index : 99999 ;
display : flex ;
align-items : center ;
justify-content : center ;
transition : opacity 0.5 s ;
}
# boot-text {
font-family : var ( - - font - mono ) ;
color : var ( - - terminal - green ) ;
text-shadow : var ( - - glow - green ) ;
font-size : 0.95 rem ;
line-height : 1.6 ;
white-space : pre ;
padding : 2 rem ;
}
2026-01-07 22:41:29 -05:00
/* Formatted Log Entry Styles */
. log-entry {
background : #000 ;
border : 1 px solid var ( - - terminal - green ) ;
border-left : 3 px solid var ( - - terminal - green ) ;
padding : 12 px ;
margin-bottom : 12 px ;
font-family : var ( - - font - mono ) ;
font-size : 0.9 em ;
}
. log-entry . success {
border-left-color : var ( - - terminal - green ) ;
}
. log-entry . failed {
border-left-color : #ff4444 ;
}
. log-timestamp {
color : var ( - - terminal - amber ) ;
font-size : 0.85 em ;
margin-bottom : 6 px ;
}
. log-title {
color : var ( - - terminal - green ) ;
font-weight : bold ;
text-shadow : var ( - - glow - green ) ;
margin-bottom : 8 px ;
font-size : 1.1 em ;
}
. log-entry . failed . log-title {
color : #ff4444 ;
text-shadow : 0 0 5 px #ff4444 ;
}
. log-details {
margin-left : 20 px ;
}
. log-field {
margin : 6 px 0 ;
color : var ( - - terminal - green ) ;
}
. log-label {
color : var ( - - terminal - amber ) ;
font-weight : bold ;
margin-right : 8 px ;
}
. log-output {
background : #0a0a0a ;
border : 1 px solid #003300 ;
padding : 10 px ;
margin : 6 px 0 ;
color : var ( - - terminal - green ) ;
font-family : var ( - - font - mono ) ;
font-size : 0.9 em ;
overflow-x : auto ;
max-height : 300 px ;
overflow-y : auto ;
}
. log-error {
color : #ff6666 ;
border-color : #330000 ;
}
. log-entry code {
background : #001a00 ;
padding : 2 px 6 px ;
border : 1 px solid #003300 ;
color : var ( - - terminal - green ) ;
font-family : var ( - - font - mono ) ;
}
2026-01-07 22:43:13 -05:00
/* Worker Metadata Styles */
. worker-stats {
display : flex ;
gap : 15 px ;
margin-top : 8 px ;
font-size : 0.85 em ;
color : var ( - - terminal - green ) ;
font-family : var ( - - font - mono ) ;
}
. worker-stats span {
padding : 2 px 6 px ;
background : #001a00 ;
border : 1 px solid #003300 ;
}
. worker-metadata {
margin-top : 12 px ;
padding : 10 px ;
background : #001a00 ;
border : 1 px solid #003300 ;
font-family : var ( - - font - mono ) ;
font-size : 0.85 em ;
}
. meta-row {
display : flex ;
padding : 4 px 0 ;
color : var ( - - terminal - green ) ;
}
. meta-label {
color : var ( - - terminal - amber ) ;
min-width : 120 px ;
font-weight : bold ;
}
2026-01-07 22:52:51 -05:00
/* Terminal Cursor Blink */
@ keyframes cursor-blink {
0 % , 49 % { opacity : 1 ; }
50 % , 100 % { opacity : 0 ; }
}
. terminal-cursor :: after {
content : '▋' ;
animation : cursor - blink 1 s step-end infinite ;
color : var ( - - terminal - green ) ;
}
/* Hover effects for execution items */
. execution-item {
transition : all 0.2 s ease ;
cursor : pointer ;
}
. execution-item : hover {
background : #001a00 ;
border-left-width : 5 px ;
transform : translateX ( 3 px ) ;
}
. worker-item : hover {
background : #001a00 ;
border-left-width : 5 px ;
}
. workflow-item : hover {
background : #001a00 ;
border-left-width : 5 px ;
}
/* Loading pulse effect */
. loading {
animation : loading-pulse 1.5 s ease-in-out infinite ;
}
@ keyframes loading-pulse {
0 % , 100 % { opacity : 0.6 ; }
50 % { opacity : 1 ; }
}
/* Success/Error message animations */
@ keyframes slide-in {
from {
opacity : 0 ;
transform : translateY ( -10 px ) ;
}
to {
opacity : 1 ;
transform : translateY ( 0 ) ;
}
}
. log-entry {
animation : slide-in 0.3 s ease-out ;
}
2025-11-29 19:26:20 -05:00
< / style >
< / head >
< body >
< div class = "container" >
< div class = "header" >
< div class = "header-left" >
< h1 > ⚡ PULSE< / h1 >
< p > Pipelined Unified Logic & Server Engine< / p >
< / div >
< div class = "user-info" id = "userInfo" >
< div class = "loading" > Loading user...< / div >
< / div >
< / div >
2025-11-30 13:03:18 -05:00
< div class = "tabs" >
< button class = "tab active" onclick = "switchTab('dashboard')" > 📊 Dashboard< / button >
< button class = "tab" onclick = "switchTab('workers')" > 👥 Workers< / button >
< button class = "tab" onclick = "switchTab('workflows')" > 📋 Workflows< / button >
< button class = "tab" onclick = "switchTab('executions')" > 🚀 Executions< / button >
< button class = "tab" onclick = "switchTab('quickcommand')" > ⚡ Quick Command< / button >
2026-01-07 23:13:27 -05:00
< button class = "tab" onclick = "switchTab('scheduler')" > ⏰ Scheduler< / button >
2025-11-29 19:26:20 -05:00
< / div >
2025-11-30 13:03:18 -05:00
<!-- Dashboard Tab -->
< div id = "dashboard" class = "tab-content active" >
< div class = "grid" >
< div class = "card" >
< h3 > 👥 Active Workers< / h3 >
< div id = "dashWorkers" > < div class = "loading" > Loading...< / div > < / div >
< / div >
< div class = "card" >
< h3 > 🚀 Recent Executions< / h3 >
< div id = "dashExecutions" > < div class = "loading" > Loading...< / div > < / div >
2025-11-29 19:26:20 -05:00
< / div >
< / div >
2025-11-30 13:03:18 -05:00
< / div >
2025-11-29 19:26:20 -05:00
2025-11-30 13:03:18 -05:00
<!-- Workers Tab -->
< div id = "workers" class = "tab-content" >
2025-11-29 19:26:20 -05:00
< div class = "card" >
2025-11-30 13:03:18 -05:00
< h3 > Worker Management< / h3 >
< button onclick = "refreshData()" > 🔄 Refresh< / button >
< div id = "workerList" > < div class = "loading" > Loading...< / div > < / div >
2025-11-29 19:26:20 -05:00
< / div >
2025-11-30 13:03:18 -05:00
< / div >
2025-11-29 19:26:20 -05:00
2025-11-30 13:03:18 -05:00
<!-- Workflows Tab -->
< div id = "workflows" class = "tab-content" >
2025-11-29 19:26:20 -05:00
< div class = "card" >
2025-11-30 13:03:18 -05:00
< h3 > Workflow Management< / h3 >
< button onclick = "showCreateWorkflow()" > ➕ Create Workflow< / button >
< button onclick = "refreshData()" > 🔄 Refresh< / button >
< div id = "workflowList" > < div class = "loading" > Loading...< / div > < / div >
2025-11-29 19:26:20 -05:00
< / div >
< / div >
2025-11-30 13:03:18 -05:00
<!-- Executions Tab -->
< div id = "executions" class = "tab-content" >
< div class = "card" >
< h3 > Execution History< / h3 >
2026-01-07 23:06:43 -05:00
2026-03-03 16:04:22 -05:00
<!-- Manual / Automated sub - tabs -->
< div style = "display: flex; gap: 0; margin-bottom: 20px; border: 2px solid var(--terminal-green);" >
< button id = "subTabManual"
onclick = "setExecutionView('manual')"
style = "flex:1; padding:10px 16px; background:rgba(0,255,65,0.2); border:none; border-right:2px solid var(--terminal-green); color:var(--terminal-amber); font-family:var(--font-mono); font-size:0.9em; cursor:pointer; text-shadow: 0 0 5px #ffb000;" >
[ 👤 Manual Runs < span id = "countManual" > < / span > ]
< / button >
< button id = "subTabAutomated"
onclick = "setExecutionView('automated')"
style = "flex:1; padding:10px 16px; background:transparent; border:none; color:var(--terminal-green); font-family:var(--font-mono); font-size:0.9em; cursor:pointer;" >
[ 🤖 Automated < span id = "countAutomated" > < / span > ]
< / button >
< / div >
2026-01-07 23:06:43 -05:00
<!-- Search and Filter Section -->
< div style = "background: rgba(0, 255, 65, 0.05); border: 2px solid var(--terminal-green); padding: 15px; margin-bottom: 20px;" >
< div style = "display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;" >
<!-- Search Box -->
< div >
< label style = "display: block; margin-bottom: 8px; font-weight: 600; color: var(--terminal-amber);" > 🔍 Search:< / label >
< input type = "text" id = "executionSearch" placeholder = "Search by command, execution ID, or workflow name..."
oninput = "filterExecutions()"
style = "width: 100%; padding: 10px; margin: 0;" >
< / div >
<!-- Status Filter -->
< div >
< label style = "display: block; margin-bottom: 8px; font-weight: 600; color: var(--terminal-amber);" > 📊 Status Filter:< / label >
< select id = "statusFilter" onchange = "filterExecutions()" style = "width: 100%; padding: 10px; margin: 0;" >
< option value = "" > All Statuses< / option >
< option value = "running" > Running< / option >
< option value = "completed" > Completed< / option >
< option value = "failed" > Failed< / option >
< option value = "waiting" > Waiting< / option >
< / select >
< / div >
< / div >
< div style = "display: flex; gap: 10px; align-items: center;" >
< button onclick = "clearFilters()" class = "small" > [ Clear Filters ]< / button >
< span id = "filterStats" style = "color: var(--terminal-green); font-size: 0.9em; font-family: var(--font-mono);" > < / span >
< / div >
< / div >
2026-01-07 22:36:51 -05:00
< button onclick = "refreshData()" > [ 🔄 Refresh ]< / button >
< button onclick = "clearCompletedExecutions()" style = "margin-left: 10px;" > [ 🗑️ Clear Completed ]< / button >
2026-01-07 23:08:55 -05:00
< button onclick = "toggleCompareMode()" id = "compareModeBtn" style = "margin-left: 10px;" > [ 📊 Compare Mode ]< / button >
< button onclick = "compareSelectedExecutions()" id = "compareBtn" style = "margin-left: 10px; display: none;" > [ ⚖️ Compare Selected ]< / button >
< div id = "compareInstructions" style = "display: none; background: rgba(255, 176, 0, 0.1); border: 2px solid var(--terminal-amber); padding: 12px; margin: 15px 0; color: var(--terminal-amber);" >
Select 2-5 executions to compare their outputs. Click executions to toggle selection.
< / div >
2025-11-30 13:03:18 -05:00
< div id = "executionList" > < div class = "loading" > Loading...< / div > < / div >
< / div >
< / div >
<!-- Quick Command Tab -->
< div id = "quickcommand" class = "tab-content" >
< div class = "card" >
< h3 > ⚡ Quick Command Execution< / h3 >
2026-01-07 22:45:40 -05:00
< p style = "color: var(--terminal-green); margin-bottom: 20px;" > Execute a command on selected workers instantly< / p >
< div style = "display: flex; gap: 10px; margin-bottom: 15px;" >
< button onclick = "showCommandTemplates()" style = "flex: 0;" > [ 📋 Templates ]< / button >
< button onclick = "showCommandHistory()" style = "flex: 0;" > [ 🕐 History ]< / button >
< / div >
2026-01-07 23:03:45 -05:00
< label style = "display: block; margin-bottom: 10px; font-weight: 600;" > Execution Mode:< / label >
< div style = "margin-bottom: 20px;" >
< label style = "display: inline-flex; align-items: center; margin-right: 20px; cursor: pointer;" >
< input type = "radio" name = "execMode" value = "single" checked onchange = "toggleWorkerSelection()" style = "width: auto; margin-right: 8px;" >
< span > Single Worker< / span >
< / label >
< label style = "display: inline-flex; align-items: center; cursor: pointer;" >
< input type = "radio" name = "execMode" value = "multi" onchange = "toggleWorkerSelection()" style = "width: auto; margin-right: 8px;" >
< span > Multiple Workers< / span >
< / label >
< / div >
< div id = "singleWorkerMode" >
< label style = "display: block; margin-bottom: 10px; font-weight: 600;" > Select Worker:< / label >
< select id = "quickWorkerSelect" >
< option value = "" > Loading workers...< / option >
< / select >
< / div >
< div id = "multiWorkerMode" style = "display: none;" >
< label style = "display: block; margin-bottom: 10px; font-weight: 600;" > Select Workers:< / label >
< div id = "workerCheckboxList" style = "background: var(--bg-primary); border: 2px solid var(--terminal-green); padding: 15px; margin-bottom: 15px; max-height: 200px; overflow-y: auto;" >
< div class = "loading" > Loading workers...< / div >
< / div >
< div style = "margin-bottom: 15px;" >
< button onclick = "selectAllWorkers()" class = "small" > [ Select All ]< / button >
< button onclick = "selectOnlineWorkers()" class = "small" > [ Online Only ]< / button >
< button onclick = "deselectAllWorkers()" class = "small" > [ Clear All ]< / button >
< / div >
< / div >
2026-01-07 22:45:40 -05:00
2025-11-30 13:03:18 -05:00
< label style = "display: block; margin-bottom: 10px; margin-top: 20px; font-weight: 600;" > Command:< / label >
< textarea id = "quickCommand" placeholder = "Enter command to execute (e.g., 'uptime' or 'df -h')" > < / textarea >
2026-01-07 22:45:40 -05:00
< button onclick = "executeQuickCommand()" > [ ▶️ Execute Command ]< / button >
2025-11-30 13:03:18 -05:00
< div id = "quickCommandResult" style = "margin-top: 20px;" > < / div >
< / div >
< / div >
2026-01-07 23:13:27 -05:00
<!-- Scheduler Tab -->
< div id = "scheduler" class = "tab-content" >
< div class = "card" >
< h3 > ⏰ Scheduled Commands< / h3 >
< p style = "color: var(--terminal-green); margin-bottom: 20px;" > Automate command execution with flexible scheduling< / p >
< button onclick = "showCreateSchedule()" > [ ➕ Create Schedule ]< / button >
< button onclick = "refreshData()" style = "margin-left: 10px;" > [ 🔄 Refresh ]< / button >
< div id = "scheduleList" style = "margin-top: 20px;" > < div class = "loading" > Loading...< / div > < / div >
< / div >
< / div >
2025-11-30 13:03:18 -05:00
< / div >
<!-- Create Workflow Modal -->
< div id = "createWorkflowModal" class = "modal" >
< div class = "modal-content" >
< h2 > Create New Workflow< / h2 >
< input type = "text" id = "workflowName" placeholder = "Workflow Name" >
< textarea id = "workflowDescription" placeholder = "Description" > < / textarea >
< label > Workflow Definition (JSON):< / label >
< textarea id = "workflowDefinition" style = "min-height: 200px;" > {
"steps": [
{
"name": "Example Step",
"type": "execute",
"targets": ["all"],
"command": "echo 'Hello from PULSE'"
}
]
}< / textarea >
2026-03-11 23:06:09 -04:00
< label style = "display:block;margin:12px 0 6px;font-weight:600;" > Webhook URL (optional):< / label >
< input type = "url" id = "workflowWebhookUrl" placeholder = "https://example.com/webhook" >
2025-11-30 13:03:18 -05:00
< button onclick = "createWorkflow()" > Create< / button >
< button onclick = "closeModal('createWorkflowModal')" > Cancel< / button >
< / div >
< / div >
<!-- View Execution Modal -->
< div id = "viewExecutionModal" class = "modal" >
< div class = "modal-content" >
< h2 > Execution Details< / h2 >
< div id = "executionDetails" > < / div >
2026-01-07 22:45:40 -05:00
< button onclick = "closeModal('viewExecutionModal')" > [ Close ]< / button >
< / div >
< / div >
<!-- Command Templates Modal -->
< div id = "commandTemplatesModal" class = "modal" >
< div class = "modal-content" >
< h2 > Command Templates< / h2 >
< div id = "templateList" style = "max-height: 400px; overflow-y: auto;" > < / div >
< button onclick = "closeModal('commandTemplatesModal')" > [ Close ]< / button >
< / div >
< / div >
<!-- Command History Modal -->
< div id = "commandHistoryModal" class = "modal" >
< div class = "modal-content" >
< h2 > Command History< / h2 >
< div id = "historyList" style = "max-height: 400px; overflow-y: auto;" > < / div >
< button onclick = "closeModal('commandHistoryModal')" > [ Close ]< / button >
2025-11-29 19:26:20 -05:00
< / div >
< / div >
2026-01-07 23:08:55 -05:00
<!-- Compare Executions Modal -->
< div id = "compareExecutionsModal" class = "modal" >
< div class = "modal-content" style = "max-width: 90%; max-height: 90vh;" >
< h2 > ⚖️ Execution Comparison< / h2 >
< div id = "compareContent" style = "max-height: 70vh; overflow-y: auto; padding: 20px;" > < / div >
< button onclick = "closeModal('compareExecutionsModal')" > [ Close ]< / button >
< / div >
< / div >
2026-03-03 16:20:05 -05:00
<!-- Workflow Param Input Modal -->
< div id = "paramModal" style = "display:none;position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:1000;align-items:center;justify-content:center;" >
< div style = "background:#0a0a0a;border:2px solid var(--terminal-green);padding:28px 32px;min-width:380px;max-width:520px;width:90%;font-family:var(--font-mono);box-shadow:0 0 30px rgba(0,255,65,.2);" >
< h2 style = "color:var(--terminal-green);margin:0 0 6px;font-size:1.1em;letter-spacing:.05em;" > ▶ RUN WORKFLOW< / h2 >
< p style = "color:var(--terminal-amber);font-size:.75em;margin:0 0 20px;letter-spacing:.04em;" > Fill in required parameters< / p >
< div id = "paramModalForm" > < / div >
2026-03-11 23:06:09 -04:00
< div style = "margin-top:16px;display:flex;align-items:center;gap:10px;" >
< input type = "checkbox" id = "paramDryRun" style = "accent-color:var(--terminal-amber);" >
< label for = "paramDryRun" style = "color:var(--terminal-amber);font-size:.85em;cursor:pointer;" > Dry Run (simulate, no commands executed)< / label >
< / div >
2026-03-03 16:20:05 -05:00
< div style = "display:flex;gap:10px;margin-top:20px;" >
< button onclick = "submitParamForm()"
style = "flex:1;padding:8px;background:rgba(0,255,65,.1);border:1px solid var(--terminal-green);color:var(--terminal-green);font-family:var(--font-mono);cursor:pointer;font-size:.9em;" >
[ ▶ Run ]
< / button >
< button onclick = "closeParamModal()"
style = "padding:8px 16px;background:transparent;border:1px solid #555;color:#888;font-family:var(--font-mono);cursor:pointer;font-size:.9em;" >
Cancel
< / button >
< / div >
< / div >
< / div >
2026-03-03 16:55:02 -05:00
<!-- Edit Workflow Modal -->
< div id = "editWorkflowModal" class = "modal" >
< div class = "modal-content" style = "max-width: 700px; width: 95%;" >
< h2 > ✏️ Edit Workflow< / h2 >
< input type = "hidden" id = "editWorkflowId" >
< label style = "display:block;margin-bottom:6px;font-weight:600;" > Name:< / label >
< input type = "text" id = "editWorkflowName" placeholder = "Workflow Name" >
< label style = "display:block;margin:12px 0 6px;font-weight:600;" > Description:< / label >
< textarea id = "editWorkflowDescription" placeholder = "Description" style = "min-height:60px;" > < / textarea >
< label style = "display:block;margin:12px 0 6px;font-weight:600;" > Definition (JSON):< / label >
< textarea id = "editWorkflowDefinition" style = "min-height: 320px; font-family: var(--font-mono); font-size: 0.85em;" > < / textarea >
2026-03-11 23:06:09 -04:00
< label style = "display:block;margin:12px 0 6px;font-weight:600;" > Webhook URL (optional):< / label >
< input type = "url" id = "editWorkflowWebhookUrl" placeholder = "https://example.com/webhook" >
2026-03-03 16:55:02 -05:00
< div id = "editWorkflowError" style = "color:var(--terminal-red);font-size:0.85em;margin-top:8px;display:none;" > < / div >
< div style = "margin-top:16px;display:flex;gap:10px;" >
< button onclick = "saveWorkflow()" > [ 💾 Save ]< / button >
< button onclick = "closeModal('editWorkflowModal')" > Cancel< / button >
< / div >
< / div >
< / div >
2026-01-07 23:13:27 -05:00
<!-- Create Schedule Modal -->
< div id = "createScheduleModal" class = "modal" >
< div class = "modal-content" >
< h2 > Create Scheduled Command< / h2 >
< label style = "display: block; margin-bottom: 8px; font-weight: 600;" > Schedule Name:< / label >
< input type = "text" id = "scheduleName" placeholder = "e.g., Daily System Check" >
< label style = "display: block; margin-bottom: 8px; margin-top: 15px; font-weight: 600;" > Command:< / label >
< textarea id = "scheduleCommand" placeholder = "Enter command to execute" > < / textarea >
< label style = "display: block; margin-bottom: 8px; margin-top: 15px; font-weight: 600;" > Target Workers:< / label >
< div id = "scheduleWorkerList" style = "background: var(--bg-primary); border: 2px solid var(--terminal-green); padding: 15px; margin-bottom: 15px; max-height: 150px; overflow-y: auto;" >
< div class = "loading" > Loading workers...< / div >
< / div >
< label style = "display: block; margin-bottom: 8px; margin-top: 15px; font-weight: 600;" > Schedule Type:< / label >
< select id = "scheduleType" onchange = "updateScheduleInput()" >
< option value = "interval" > Every X Minutes< / option >
< option value = "hourly" > Every X Hours< / option >
< option value = "daily" > Daily at Time< / option >
< / select >
< div id = "scheduleInputContainer" style = "margin-top: 15px;" >
< label style = "display: block; margin-bottom: 8px; font-weight: 600;" > Interval (minutes):< / label >
< input type = "number" id = "scheduleValue" placeholder = "e.g., 30" min = "1" >
< / div >
< div style = "margin-top: 20px;" >
< button onclick = "createSchedule()" > [ Create Schedule ]< / button >
< button onclick = "closeModal('createScheduleModal')" style = "margin-left: 10px;" > [ Cancel ]< / button >
< / div >
< / div >
< / div >
2025-11-29 19:26:20 -05:00
< script >
let currentUser = null ;
let ws = null ;
2025-11-30 13:03:18 -05:00
let workers = [ ] ;
2026-01-07 23:06:43 -05:00
let allExecutions = [ ] ; // Store all loaded executions for filtering
2026-01-07 23:08:55 -05:00
let compareMode = false ;
2026-03-11 22:53:25 -04:00
let selectedExecutions = new Set ( ) ;
2025-11-29 19:26:20 -05:00
async function loadUser ( ) {
try {
const response = await fetch ( '/api/user' ) ;
2025-11-30 13:03:18 -05:00
if ( ! response . ok ) return false ;
2025-11-29 19:26:20 -05:00
currentUser = await response . json ( ) ;
document . getElementById ( 'userInfo' ) . innerHTML = `
2026-03-11 22:53:25 -04:00
<div class="name"> ${ escapeHtml ( currentUser . name || '' ) } </div>
<div class="email"> ${ escapeHtml ( currentUser . email || '' ) } </div>
<div> ${ ( currentUser . groups || [ ] ) . map ( g =>
` <span class="badge"> ${ escapeHtml ( g ) } </span> `
2025-11-29 19:26:20 -05:00
) . join ( '' ) } </div>
` ;
return true ;
} catch ( error ) {
console . error ( 'Error loading user:' , error ) ;
return false ;
}
}
async function loadWorkers ( ) {
try {
const response = await fetch ( '/api/workers' ) ;
2025-11-30 13:03:18 -05:00
workers = await response . json ( ) ;
2026-01-07 23:03:45 -05:00
// Update worker select in quick command (single mode)
2025-11-30 13:03:18 -05:00
const select = document . getElementById ( 'quickWorkerSelect' ) ;
if ( select ) {
2026-01-07 23:03:45 -05:00
select . innerHTML = workers . map ( w =>
2025-11-30 13:03:18 -05:00
` <option value=" ${ w . id } "> ${ w . name } ( ${ w . status } )</option> `
) . join ( '' ) ;
2025-11-29 19:26:20 -05:00
}
2026-01-07 23:03:45 -05:00
// Update worker checkboxes (multi mode)
const checkboxList = document . getElementById ( 'workerCheckboxList' ) ;
if ( checkboxList ) {
checkboxList . innerHTML = workers . length === 0 ?
'<div class="empty">No workers available</div>' :
workers . map ( w => `
<label style="display: block; margin-bottom: 10px; cursor: pointer; padding: 8px; border: 1px solid var(--terminal-green); background: ${ w . status === 'online' ? 'rgba(0, 255, 65, 0.05)' : 'transparent' } ;">
<input type="checkbox" name="workerCheckbox" value=" ${ w . id } " data-status=" ${ w . status } " style="width: auto; margin-right: 8px;">
<span class="status ${ w . status } " style="padding: 2px 8px; font-size: 0.8em;">[ ${ w . status === 'online' ? '●' : '○' } ]</span>
<strong> ${ w . name } </strong>
</label>
` ) . join ( '' ) ;
}
2025-11-30 13:03:18 -05:00
// Dashboard view
2026-01-07 22:43:13 -05:00
const dashHtml = workers . length === 0 ?
2025-11-30 13:03:18 -05:00
'<div class="empty">No workers connected</div>' :
2026-01-07 22:43:13 -05:00
workers . map ( w => {
const meta = w . metadata ? ( typeof w . metadata === 'string' ? JSON . parse ( w . metadata ) : w . metadata ) : null ;
const lastSeen = getTimeAgo ( new Date ( w . last _heartbeat ) ) ;
return `
<div class="worker-item">
<span class="status ${ w . status } ">[ ${ w . status === 'online' ? '●' : '○' } ]</span>
<strong> ${ w . name } </strong>
${ meta ? ` <div class="worker-stats">
<span>CPU: ${ meta . cpus || '?' } cores</span>
<span>RAM: ${ formatBytes ( meta . freeMem ) } / ${ formatBytes ( meta . totalMem ) } </span>
<span>Tasks: ${ meta . activeTasks || 0 } / ${ meta . maxConcurrentTasks || 0 } </span>
</div> ` : '' }
<div class="timestamp">Last seen: ${ lastSeen } </div>
</div>
` ;
} ) . join ( '' ) ;
2025-11-30 13:03:18 -05:00
document . getElementById ( 'dashWorkers' ) . innerHTML = dashHtml ;
// Full worker list
const fullHtml = workers . length === 0 ?
'<div class="empty">No workers connected</div>' :
workers . map ( w => {
2026-01-07 22:43:13 -05:00
const meta = w . metadata ? ( typeof w . metadata === 'string' ? JSON . parse ( w . metadata ) : w . metadata ) : null ;
const lastSeen = getTimeAgo ( new Date ( w . last _heartbeat ) ) ;
const memUsagePercent = meta && meta . totalMem ? ( ( meta . totalMem - meta . freeMem ) / meta . totalMem * 100 ) . toFixed ( 1 ) : 0 ;
const loadAvg = meta && meta . loadavg ? meta . loadavg . map ( l => l . toFixed ( 2 ) ) . join ( ', ' ) : 'N/A' ;
2025-11-30 13:03:18 -05:00
return `
<div class="worker-item">
<div style="display: flex; justify-content: space-between; align-items: start;">
2026-01-07 22:43:13 -05:00
<div style="flex: 1;">
<span class="status ${ w . status } ">[ ${ w . status === 'online' ? '●' : '○' } ]</span>
2025-11-30 13:03:18 -05:00
<strong> ${ w . name } </strong>
2026-01-07 22:43:13 -05:00
<div class="timestamp">Last seen: ${ lastSeen } </div>
2025-11-30 13:03:18 -05:00
${ meta ? `
2026-01-07 22:43:13 -05:00
<div class="worker-metadata">
<div class="meta-row">
<span class="meta-label">System:</span>
<span> ${ meta . platform || 'N/A' } ${ meta . arch || '' } | ${ meta . cpus || '?' } CPU cores</span>
</div>
<div class="meta-row">
<span class="meta-label">Memory:</span>
<span> ${ formatBytes ( meta . totalMem - meta . freeMem ) } / ${ formatBytes ( meta . totalMem ) } ( ${ memUsagePercent } % used)</span>
</div>
<div class="meta-row">
<span class="meta-label">Load Avg:</span>
<span> ${ loadAvg } </span>
</div>
<div class="meta-row">
<span class="meta-label">Uptime:</span>
<span> ${ formatUptime ( meta . uptime ) } </span>
</div>
<div class="meta-row">
<span class="meta-label">Active Tasks:</span>
<span> ${ meta . activeTasks || 0 } / ${ meta . maxConcurrentTasks || 0 } </span>
</div>
2025-11-30 13:03:18 -05:00
</div>
` : '' }
</div>
${ currentUser && currentUser . isAdmin ? `
2026-01-07 22:43:13 -05:00
<button class="danger small" onclick="deleteWorker(' ${ w . id } ', ' ${ w . name } ')">[ 🗑️ Delete ]</button>
2025-11-30 13:03:18 -05:00
` : '' }
</div>
</div>
` ;
} ) . join ( '' ) ;
document . getElementById ( 'workerList' ) . innerHTML = fullHtml ;
2025-11-29 19:26:20 -05:00
} catch ( error ) {
console . error ( 'Error loading workers:' , error ) ;
}
}
2026-03-03 16:20:05 -05:00
// Workflow registry (id → definition) for param lookup
let _workflowRegistry = { } ;
2025-11-29 19:26:20 -05:00
async function loadWorkflows ( ) {
try {
const response = await fetch ( '/api/workflows' ) ;
const workflows = await response . json ( ) ;
2026-01-07 23:13:27 -05:00
2026-03-03 16:20:05 -05:00
// Cache definitions for param lookup at execute time
_workflowRegistry = { } ;
workflows . forEach ( w => {
const def = typeof w . definition === 'string' ? JSON . parse ( w . definition ) : w . definition ;
_workflowRegistry [ w . id ] = def ;
} ) ;
const paramBadge = ( def ) => {
const ps = ( def && def . params ) || [ ] ;
return ps . length ? ` <span style="font-size:.75em;color:var(--terminal-amber);margin-left:6px;">[ ${ ps . length } param ${ ps . length > 1 ? 's' : '' } ]</span> ` : '' ;
} ;
2025-11-30 13:03:18 -05:00
const html = workflows . length === 0 ?
'<div class="empty">No workflows defined yet</div>' :
2026-03-03 16:20:05 -05:00
workflows . map ( w => {
const def = _workflowRegistry [ w . id ] || { } ;
return `
2025-11-30 13:03:18 -05:00
<div class="workflow-item">
2026-03-03 16:20:05 -05:00
<div class="workflow-name"> ${ w . name } ${ paramBadge ( def ) } </div>
2025-11-30 13:03:18 -05:00
<div class="workflow-desc"> ${ w . description || 'No description' } </div>
<div class="timestamp">Created by ${ w . created _by || 'Unknown' } on ${ new Date ( w . created _at ) . toLocaleString ( ) } </div>
<div style="margin-top: 10px;">
2026-03-03 16:20:05 -05:00
<button onclick="executeWorkflow(' ${ w . id } ')">▶️ Execute</button>
2026-01-07 23:13:27 -05:00
${ currentUser && currentUser . isAdmin ?
2026-03-03 16:55:02 -05:00
` <button onclick="editWorkflow(' ${ w . id } ')">✏️ Edit</button>
<button class="danger" onclick="deleteWorkflow(' ${ w . id } ', ' ${ w . name } ')">🗑️ Delete</button> `
2025-11-30 13:03:18 -05:00
: '' }
</div>
2026-03-03 16:20:05 -05:00
</div> ` ;
} ) . join ( '' ) ;
2025-11-30 13:03:18 -05:00
document . getElementById ( 'workflowList' ) . innerHTML = html ;
2025-11-29 19:26:20 -05:00
} catch ( error ) {
console . error ( 'Error loading workflows:' , error ) ;
}
}
2026-01-07 23:13:27 -05:00
async function loadSchedules ( ) {
try {
const response = await fetch ( '/api/scheduled-commands' ) ;
const schedules = await response . json ( ) ;
const html = schedules . length === 0 ?
'<div class="empty">No scheduled commands yet</div>' :
schedules . map ( s => {
const workerIds = JSON . parse ( s . worker _ids ) ;
const workerNames = workerIds . map ( id => {
const w = workers . find ( worker => worker . id === id ) ;
return w ? w . name : id . substring ( 0 , 8 ) ;
} ) . join ( ', ' ) ;
let scheduleDesc = '' ;
if ( s . schedule _type === 'interval' ) {
scheduleDesc = ` Every ${ s . schedule _value } minutes ` ;
} else if ( s . schedule _type === 'hourly' ) {
scheduleDesc = ` Every ${ s . schedule _value } hour(s) ` ;
} else if ( s . schedule _type === 'daily' ) {
scheduleDesc = ` Daily at ${ s . schedule _value } ` ;
}
const nextRun = s . next _run ? new Date ( s . next _run ) . toLocaleString ( ) : 'Not scheduled' ;
const lastRun = s . last _run ? new Date ( s . last _run ) . toLocaleString ( ) : 'Never' ;
return `
<div class="workflow-item" style="opacity: ${ s . enabled ? 1 : 0.6 } ;">
<div style="display: flex; justify-content: space-between; align-items: start;">
<div style="flex: 1;">
<div class="workflow-name"> ${ s . name } </div>
<div style="color: var(--terminal-green); font-family: var(--font-mono); font-size: 0.9em; margin: 8px 0;">
Command: <code> ${ escapeHtml ( s . command ) } </code>
</div>
<div style="color: var(--terminal-amber); font-size: 0.9em; margin-bottom: 5px;">
📅 ${ scheduleDesc }
</div>
<div style="color: var(--text-muted); font-size: 0.85em;">
Workers: ${ workerNames }
</div>
<div class="timestamp">
Last run: ${ lastRun } | Next run: ${ nextRun }
</div>
</div>
<div style="margin-left: 15px;">
<span class="status ${ s . enabled ? 'online' : 'offline' } " style="font-size: 0.85em;">
${ s . enabled ? 'ENABLED' : 'DISABLED' }
</span>
</div>
</div>
<div style="margin-top: 10px;">
<button onclick="toggleSchedule(' ${ s . id } ')" class="small">
${ s . enabled ? '⏸️ Disable' : '▶️ Enable' }
</button>
<button class="danger small" onclick="deleteSchedule(' ${ s . id } ', ' ${ s . name } ')">🗑️ Delete</button>
</div>
</div>
` ;
} ) . join ( '' ) ;
document . getElementById ( 'scheduleList' ) . innerHTML = html ;
} catch ( error ) {
console . error ( 'Error loading schedules:' , error ) ;
}
}
function showCreateSchedule ( ) {
// Populate worker checkboxes
const workerList = document . getElementById ( 'scheduleWorkerList' ) ;
workerList . innerHTML = workers . length === 0 ?
'<div class="empty">No workers available</div>' :
workers . map ( w => `
<label style="display: block; margin-bottom: 10px; cursor: pointer; padding: 8px; border: 1px solid var(--terminal-green);">
<input type="checkbox" name="scheduleWorkerCheckbox" value=" ${ w . id } " style="width: auto; margin-right: 8px;">
<span class="status ${ w . status } " style="padding: 2px 8px; font-size: 0.8em;">[ ${ w . status === 'online' ? '●' : '○' } ]</span>
<strong> ${ w . name } </strong>
</label>
` ) . join ( '' ) ;
document . getElementById ( 'createScheduleModal' ) . classList . add ( 'show' ) ;
}
function updateScheduleInput ( ) {
const scheduleType = document . getElementById ( 'scheduleType' ) . value ;
const container = document . getElementById ( 'scheduleInputContainer' ) ;
if ( scheduleType === 'interval' ) {
container . innerHTML = `
<label style="display: block; margin-bottom: 8px; font-weight: 600;">Interval (minutes):</label>
<input type="number" id="scheduleValue" placeholder="e.g., 30" min="1">
` ;
} else if ( scheduleType === 'hourly' ) {
container . innerHTML = `
<label style="display: block; margin-bottom: 8px; font-weight: 600;">Every X Hours:</label>
<input type="number" id="scheduleValue" placeholder="e.g., 2" min="1" max="24">
` ;
} else if ( scheduleType === 'daily' ) {
container . innerHTML = `
<label style="display: block; margin-bottom: 8px; font-weight: 600;">Time (HH:MM):</label>
<input type="time" id="scheduleValue">
` ;
}
}
async function createSchedule ( ) {
const name = document . getElementById ( 'scheduleName' ) . value ;
const command = document . getElementById ( 'scheduleCommand' ) . value ;
const scheduleType = document . getElementById ( 'scheduleType' ) . value ;
const scheduleValue = document . getElementById ( 'scheduleValue' ) . value ;
const selectedWorkers = Array . from ( document . querySelectorAll ( 'input[name="scheduleWorkerCheckbox"]:checked' ) ) . map ( cb => cb . value ) ;
if ( ! name || ! command || ! scheduleValue || selectedWorkers . length === 0 ) {
alert ( 'Please fill in all fields and select at least one worker' ) ;
return ;
}
try {
const response = await fetch ( '/api/scheduled-commands' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( {
name ,
command ,
worker _ids : selectedWorkers ,
schedule _type : scheduleType ,
schedule _value : scheduleValue
} )
} ) ;
if ( response . ok ) {
closeModal ( 'createScheduleModal' ) ;
showTerminalNotification ( 'Schedule created successfully' , 'success' ) ;
loadSchedules ( ) ;
// Clear form
document . getElementById ( 'scheduleName' ) . value = '' ;
document . getElementById ( 'scheduleCommand' ) . value = '' ;
document . getElementById ( 'scheduleValue' ) . value = '' ;
} else {
const error = await response . json ( ) ;
showTerminalNotification ( 'Failed to create schedule: ' + error . error , 'error' ) ;
}
} catch ( error ) {
console . error ( 'Error creating schedule:' , error ) ;
showTerminalNotification ( 'Error creating schedule' , 'error' ) ;
}
}
async function toggleSchedule ( scheduleId ) {
try {
const response = await fetch ( ` /api/scheduled-commands/ ${ scheduleId } /toggle ` , {
method : 'PUT'
} ) ;
if ( response . ok ) {
const data = await response . json ( ) ;
showTerminalNotification ( ` Schedule ${ data . enabled ? 'enabled' : 'disabled' } ` , 'success' ) ;
loadSchedules ( ) ;
} else {
showTerminalNotification ( 'Failed to toggle schedule' , 'error' ) ;
}
} catch ( error ) {
console . error ( 'Error toggling schedule:' , error ) ;
showTerminalNotification ( 'Error toggling schedule' , 'error' ) ;
}
}
async function deleteSchedule ( scheduleId , name ) {
if ( ! confirm ( ` Delete scheduled command: ${ name } ? ` ) ) return ;
try {
const response = await fetch ( ` /api/scheduled-commands/ ${ scheduleId } ` , {
method : 'DELETE'
} ) ;
if ( response . ok ) {
showTerminalNotification ( 'Schedule deleted' , 'success' ) ;
loadSchedules ( ) ;
} else {
showTerminalNotification ( 'Failed to delete schedule' , 'error' ) ;
}
} catch ( error ) {
console . error ( 'Error deleting schedule:' , error ) ;
showTerminalNotification ( 'Error deleting schedule' , 'error' ) ;
}
}
2026-01-07 22:50:39 -05:00
let executionOffset = 0 ;
const executionLimit = 50 ;
2026-03-03 16:04:22 -05:00
let executionView = localStorage . getItem ( 'pulse_executionView' ) || 'manual' ;
2026-01-07 22:50:39 -05:00
async function loadExecutions ( append = false ) {
2025-11-29 19:26:20 -05:00
try {
2026-01-07 22:50:39 -05:00
if ( ! append ) executionOffset = 0 ;
const response = await fetch ( ` /api/executions?limit= ${ executionLimit } &offset= ${ executionOffset } ` ) ;
const data = await response . json ( ) ;
const executions = data . executions || data ; // Handle old and new API format
2026-01-07 20:24:11 -05:00
2026-01-07 23:06:43 -05:00
// Store executions for filtering
if ( append ) {
allExecutions = allExecutions . concat ( executions ) ;
} else {
allExecutions = executions ;
}
2026-03-03 16:04:22 -05:00
// Dashboard view (first 5 manual runs only)
2026-01-07 22:50:39 -05:00
if ( ! append ) {
2026-03-03 16:04:22 -05:00
const manualExecs = executions . filter ( e => ! isAutomatedRun ( e ) ) ;
const dashHtml = manualExecs . length === 0 ?
2026-01-07 22:50:39 -05:00
'<div class="empty">No executions yet</div>' :
2026-03-03 16:04:22 -05:00
manualExecs . slice ( 0 , 5 ) . map ( e => `
2026-01-07 22:50:39 -05:00
<div class="execution-item" onclick="viewExecution(' ${ e . id } ')">
<span class="status ${ e . status } "> ${ e . status } </span>
<strong> ${ e . workflow _name || '[Quick Command]' } </strong>
<div class="timestamp">by ${ e . started _by } at ${ new Date ( e . started _at ) . toLocaleString ( ) } </div>
</div>
` ) . join ( '' ) ;
document . getElementById ( 'dashExecutions' ) . innerHTML = dashHtml ;
}
2026-01-07 23:06:43 -05:00
// Apply filters and render
renderFilteredExecutions ( ) ;
2026-01-07 22:50:39 -05:00
// Add "Load More" button if there are more executions
if ( data . hasMore ) {
const loadMoreBtn = ` <button onclick="loadMoreExecutions()" style="width: 100%; margin-top: 15px;">[ Load More Executions ]</button> ` ;
document . getElementById ( 'executionList' ) . innerHTML += loadMoreBtn ;
}
2025-11-29 19:26:20 -05:00
} catch ( error ) {
console . error ( 'Error loading executions:' , error ) ;
}
}
2026-03-03 16:04:22 -05:00
function isAutomatedRun ( e ) {
const by = e . started _by || '' ;
return by . startsWith ( 'gandalf:' ) || by . startsWith ( 'scheduler:' ) ;
}
function updateSubTabCounts ( ) {
const manual = allExecutions . filter ( e => ! isAutomatedRun ( e ) ) . length ;
const automated = allExecutions . filter ( e => isAutomatedRun ( e ) ) . length ;
const cm = document . getElementById ( 'countManual' ) ;
const ca = document . getElementById ( 'countAutomated' ) ;
if ( cm ) cm . textContent = manual ? ` ( ${ manual } ) ` : '' ;
if ( ca ) ca . textContent = automated ? ` ( ${ automated } ) ` : '' ;
}
function setExecutionView ( view ) {
executionView = view ;
localStorage . setItem ( 'pulse_executionView' , view ) ;
const manualBtn = document . getElementById ( 'subTabManual' ) ;
const autoBtn = document . getElementById ( 'subTabAutomated' ) ;
if ( manualBtn && autoBtn ) {
if ( view === 'manual' ) {
manualBtn . style . background = 'rgba(0,255,65,0.2)' ;
manualBtn . style . color = 'var(--terminal-amber)' ;
manualBtn . style . textShadow = '0 0 5px #ffb000' ;
autoBtn . style . background = 'transparent' ;
autoBtn . style . color = 'var(--terminal-green)' ;
autoBtn . style . textShadow = 'none' ;
} else {
autoBtn . style . background = 'rgba(0,255,65,0.2)' ;
autoBtn . style . color = 'var(--terminal-amber)' ;
autoBtn . style . textShadow = '0 0 5px #ffb000' ;
manualBtn . style . background = 'transparent' ;
manualBtn . style . color = 'var(--terminal-green)' ;
manualBtn . style . textShadow = 'none' ;
}
}
renderFilteredExecutions ( ) ;
}
2026-01-07 23:06:43 -05:00
function renderFilteredExecutions ( ) {
const searchTerm = ( document . getElementById ( 'executionSearch' ) ? . value || '' ) . toLowerCase ( ) ;
const statusFilter = document . getElementById ( 'statusFilter' ) ? . value || '' ;
2026-03-03 16:04:22 -05:00
updateSubTabCounts ( ) ;
2026-01-07 23:06:43 -05:00
// Filter executions
let filtered = allExecutions . filter ( e => {
2026-03-03 16:04:22 -05:00
// View filter (manual vs automated)
if ( executionView === 'manual' && isAutomatedRun ( e ) ) return false ;
if ( executionView === 'automated' && ! isAutomatedRun ( e ) ) return false ;
2026-01-07 23:06:43 -05:00
// Status filter
if ( statusFilter && e . status !== statusFilter ) return false ;
// Search filter (search in workflow name, execution ID, and logs)
if ( searchTerm ) {
const workflowName = ( e . workflow _name || '[Quick Command]' ) . toLowerCase ( ) ;
const executionId = e . id . toLowerCase ( ) ;
// Try to extract command from logs if it's a quick command
let commandText = '' ;
try {
const logs = typeof e . logs === 'string' ? JSON . parse ( e . logs ) : e . logs ;
if ( logs && logs . length > 0 && logs [ 0 ] . command ) {
commandText = logs [ 0 ] . command . toLowerCase ( ) ;
}
} catch ( err ) {
// Ignore parsing errors
}
const matchFound = workflowName . includes ( searchTerm ) ||
executionId . includes ( searchTerm ) ||
commandText . includes ( searchTerm ) ;
if ( ! matchFound ) return false ;
}
return true ;
} ) ;
// Update filter stats
const statsEl = document . getElementById ( 'filterStats' ) ;
if ( statsEl ) {
if ( searchTerm || statusFilter ) {
statsEl . textContent = ` Showing ${ filtered . length } of ${ allExecutions . length } executions ` ;
} else {
statsEl . textContent = '' ;
}
}
// Render filtered results
const fullHtml = filtered . length === 0 ?
'<div class="empty">No executions match your filters</div>' :
2026-01-07 23:08:55 -05:00
filtered . map ( e => {
2026-03-11 22:53:25 -04:00
const isSelected = selectedExecutions . has ( e . id ) ;
2026-01-07 23:08:55 -05:00
const clickHandler = compareMode ? ` toggleExecutionSelection(' ${ e . id } ') ` : ` viewExecution(' ${ e . id } ') ` ;
const selectedStyle = isSelected ? 'background: rgba(255, 176, 0, 0.2); border-left-width: 5px; border-left-color: var(--terminal-amber);' : '' ;
return `
<div class="execution-item" onclick=" ${ clickHandler } " style=" ${ selectedStyle } cursor: pointer;">
${ compareMode && isSelected ? '<span style="color: var(--terminal-amber); margin-right: 8px;">✓</span>' : '' }
<span class="status ${ e . status } "> ${ e . status } </span>
<strong> ${ e . workflow _name || '[Quick Command]' } </strong>
<div class="timestamp">
Started by ${ e . started _by } at ${ new Date ( e . started _at ) . toLocaleString ( ) }
${ e . completed _at ? ` • Completed at ${ new Date ( e . completed _at ) . toLocaleString ( ) } ` : '' }
</div>
2026-01-07 23:06:43 -05:00
</div>
2026-01-07 23:08:55 -05:00
` ;
} ) . join ( '' ) ;
2026-01-07 23:06:43 -05:00
document . getElementById ( 'executionList' ) . innerHTML = fullHtml ;
}
function filterExecutions ( ) {
renderFilteredExecutions ( ) ;
}
function clearFilters ( ) {
document . getElementById ( 'executionSearch' ) . value = '' ;
document . getElementById ( 'statusFilter' ) . value = '' ;
renderFilteredExecutions ( ) ;
}
2026-01-07 23:08:55 -05:00
function toggleCompareMode ( ) {
compareMode = ! compareMode ;
2026-03-11 22:53:25 -04:00
selectedExecutions = new Set ( ) ;
2026-01-07 23:08:55 -05:00
const btn = document . getElementById ( 'compareModeBtn' ) ;
const compareBtn = document . getElementById ( 'compareBtn' ) ;
const instructions = document . getElementById ( 'compareInstructions' ) ;
if ( compareMode ) {
btn . textContent = '[ ✗ Exit Compare Mode ]' ;
btn . style . borderColor = 'var(--terminal-amber)' ;
btn . style . color = 'var(--terminal-amber)' ;
compareBtn . style . display = 'inline-block' ;
instructions . style . display = 'block' ;
} else {
btn . textContent = '[ 📊 Compare Mode ]' ;
btn . style . borderColor = '' ;
btn . style . color = '' ;
compareBtn . style . display = 'none' ;
instructions . style . display = 'none' ;
}
renderFilteredExecutions ( ) ;
}
function toggleExecutionSelection ( executionId ) {
2026-03-11 22:53:25 -04:00
if ( selectedExecutions . has ( executionId ) ) {
selectedExecutions . delete ( executionId ) ;
2026-01-07 23:08:55 -05:00
} else {
2026-03-11 22:53:25 -04:00
if ( selectedExecutions . size >= 5 ) {
2026-01-07 23:08:55 -05:00
showTerminalNotification ( 'Maximum 5 executions can be compared' , 'error' ) ;
return ;
}
2026-03-11 22:53:25 -04:00
selectedExecutions . add ( executionId ) ;
2026-01-07 23:08:55 -05:00
}
renderFilteredExecutions ( ) ;
// Update compare button text
const compareBtn = document . getElementById ( 'compareBtn' ) ;
2026-03-11 22:53:25 -04:00
if ( selectedExecutions . size >= 2 ) {
compareBtn . textContent = ` [ ⚖️ Compare Selected ( ${ selectedExecutions . size } ) ] ` ;
2026-01-07 23:08:55 -05:00
} else {
compareBtn . textContent = '[ ⚖️ Compare Selected ]' ;
}
}
async function compareSelectedExecutions ( ) {
2026-03-11 22:53:25 -04:00
if ( selectedExecutions . size < 2 ) {
2026-01-07 23:08:55 -05:00
showTerminalNotification ( 'Please select at least 2 executions to compare' , 'error' ) ;
return ;
}
// Fetch detailed data for all selected executions
const executionDetails = [ ] ;
for ( const execId of selectedExecutions ) {
try {
const response = await fetch ( ` /api/executions/ ${ execId } ` ) ;
if ( response . ok ) {
executionDetails . push ( await response . json ( ) ) ;
}
} catch ( error ) {
console . error ( 'Error fetching execution:' , error ) ;
}
}
if ( executionDetails . length < 2 ) {
showTerminalNotification ( 'Failed to load execution details' , 'error' ) ;
return ;
}
// Generate comparison view
let comparisonHtml = '<div style="display: grid; gap: 20px;">' ;
// Summary table
comparisonHtml += `
<div style="background: rgba(0, 255, 65, 0.05); border: 2px solid var(--terminal-green); padding: 15px;">
<h3 style="margin-top: 0; color: var(--terminal-amber);">Comparison Summary</h3>
<table style="width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 0.9em;">
<thead>
<tr style="border-bottom: 2px solid var(--terminal-green);">
<th style="text-align: left; padding: 8px; color: var(--terminal-amber);">Execution</th>
<th style="text-align: left; padding: 8px; color: var(--terminal-amber);">Status</th>
<th style="text-align: left; padding: 8px; color: var(--terminal-amber);">Started</th>
<th style="text-align: left; padding: 8px; color: var(--terminal-amber);">Duration</th>
</tr>
</thead>
<tbody>
${ executionDetails . map ( ( exec , idx ) => {
const duration = exec . completed _at ?
Math . round ( ( new Date ( exec . completed _at ) - new Date ( exec . started _at ) ) / 1000 ) + 's' :
'Running...' ;
return `
<tr style="border-bottom: 1px solid #003300;">
<td style="padding: 8px; color: var(--terminal-green);">Execution ${ idx + 1 } </td>
<td style="padding: 8px;"><span class="status ${ exec . status } " style="font-size: 0.85em;"> ${ exec . status } </span></td>
<td style="padding: 8px; color: var(--terminal-green);"> ${ new Date ( exec . started _at ) . toLocaleString ( ) } </td>
<td style="padding: 8px; color: var(--terminal-green);"> ${ duration } </td>
</tr>
` ;
} ).join('')}
</tbody>
</table>
</div>
` ;
// Side-by-side output comparison
comparisonHtml += `
<div style="background: rgba(0, 255, 65, 0.05); border: 2px solid var(--terminal-green); padding: 15px;">
<h3 style="margin-top: 0; color: var(--terminal-amber);">Output Comparison</h3>
<div style="display: grid; grid-template-columns: repeat( ${ executionDetails . length } , 1fr); gap: 15px;">
${ executionDetails . map ( ( exec , idx ) => {
const logs = typeof exec . logs === 'string' ? JSON . parse ( exec . logs ) : exec . logs ;
const resultLog = logs . find ( l => l . action === 'command_result' ) ;
const stdout = resultLog ? . stdout || 'No output' ;
const stderr = resultLog ? . stderr || '' ;
return `
<div style="border: 2px solid var(--terminal-green); background: #000;">
<div style="background: var(--bg-secondary); padding: 10px; border-bottom: 2px solid var(--terminal-green);">
<strong style="color: var(--terminal-amber);">Execution ${ idx + 1 } </strong>
<div style="font-size: 0.85em; color: var(--terminal-green);">
${ exec . workflow _name || '[Quick Command]' }
</div>
</div>
<div style="padding: 12px;">
${ stdout ? `
<div style="margin-bottom: 10px;">
<div style="color: var(--terminal-amber); font-weight: bold; margin-bottom: 5px;">STDOUT:</div>
<pre style="margin: 0; color: var(--terminal-green); font-size: 0.85em; max-height: 400px; overflow-y: auto; white-space: pre-wrap;"> ${ escapeHtml ( stdout ) } </pre>
</div>
` : '' }
${ stderr ? `
<div>
<div style="color: #ff4444; font-weight: bold; margin-bottom: 5px;">STDERR:</div>
<pre style="margin: 0; color: #ff4444; font-size: 0.85em; max-height: 200px; overflow-y: auto; white-space: pre-wrap;"> ${ escapeHtml ( stderr ) } </pre>
</div>
` : '' }
</div>
</div>
` ;
} ).join('')}
</div>
</div>
` ;
// Diff analysis (simple line-by-line comparison for 2 executions)
if ( executionDetails . length === 2 ) {
const logs1 = typeof executionDetails [ 0 ] . logs === 'string' ? JSON . parse ( executionDetails [ 0 ] . logs ) : executionDetails [ 0 ] . logs ;
const logs2 = typeof executionDetails [ 1 ] . logs === 'string' ? JSON . parse ( executionDetails [ 1 ] . logs ) : executionDetails [ 1 ] . logs ;
const result1 = logs1 . find ( l => l . action === 'command_result' ) ;
const result2 = logs2 . find ( l => l . action === 'command_result' ) ;
const stdout1 = result1 ? . stdout || '' ;
const stdout2 = result2 ? . stdout || '' ;
const lines1 = stdout1 . split ( '\n' ) ;
const lines2 = stdout2 . split ( '\n' ) ;
const maxLines = Math . max ( lines1 . length , lines2 . length ) ;
let diffLines = [ ] ;
let identicalCount = 0 ;
let differentCount = 0 ;
for ( let i = 0 ; i < maxLines ; i ++ ) {
const line1 = lines1 [ i ] || '' ;
const line2 = lines2 [ i ] || '' ;
if ( line1 === line2 ) {
identicalCount ++ ;
diffLines . push ( ` <div style="color: #666; padding: 2px;"> ${ i + 1 } : ${ escapeHtml ( line1 ) || '(empty)' } </div> ` ) ;
} else {
differentCount ++ ;
diffLines . push ( `
<div style="background: rgba(255, 176, 0, 0.1); border-left: 3px solid var(--terminal-amber); padding: 2px; margin: 2px 0;">
<div style="color: var(--terminal-green);"> ${ i + 1 } [Exec 1]: ${ escapeHtml ( line1 ) || '(empty)' } </div>
<div style="color: var(--terminal-amber);"> ${ i + 1 } [Exec 2]: ${ escapeHtml ( line2 ) || '(empty)' } </div>
</div>
` ) ;
}
}
comparisonHtml += `
<div style="background: rgba(0, 255, 65, 0.05); border: 2px solid var(--terminal-green); padding: 15px;">
<h3 style="margin-top: 0; color: var(--terminal-amber);">Diff Analysis</h3>
<div style="margin-bottom: 10px; font-family: var(--font-mono); font-size: 0.9em;">
<span style="color: var(--terminal-green);">✓ Identical lines: ${ identicalCount } </span> |
<span style="color: var(--terminal-amber);">≠ Different lines: ${ differentCount } </span>
</div>
<div style="background: #000; border: 2px solid var(--terminal-green); padding: 10px; max-height: 400px; overflow-y: auto; font-family: var(--font-mono); font-size: 0.85em;">
${ diffLines . join ( '' ) }
</div>
</div>
` ;
}
comparisonHtml += '</div>' ;
document . getElementById ( 'compareContent' ) . innerHTML = comparisonHtml ;
document . getElementById ( 'compareExecutionsModal' ) . classList . add ( 'show' ) ;
}
2026-01-07 22:50:39 -05:00
async function loadMoreExecutions ( ) {
executionOffset += executionLimit ;
await loadExecutions ( true ) ;
}
2026-01-07 22:36:51 -05:00
async function clearCompletedExecutions ( ) {
if ( ! confirm ( 'Delete all completed and failed executions?' ) ) return ;
try {
2026-03-11 22:53:25 -04:00
const response = await fetch ( '/api/executions?limit=9999' ) ; // Get all executions
2026-01-07 22:55:13 -05:00
const data = await response . json ( ) ;
const executions = data . executions || data ; // Handle new pagination format
2026-01-07 22:36:51 -05:00
const toDelete = executions . filter ( e => e . status === 'completed' || e . status === 'failed' ) ;
if ( toDelete . length === 0 ) {
alert ( 'No completed or failed executions to delete' ) ;
return ;
}
2026-01-07 22:55:13 -05:00
let deleted = 0 ;
2026-01-07 22:36:51 -05:00
for ( const execution of toDelete ) {
2026-01-07 22:55:13 -05:00
const deleteResponse = await fetch ( ` /api/executions/ ${ execution . id } ` , { method : 'DELETE' } ) ;
if ( deleteResponse . ok ) deleted ++ ;
2026-01-07 22:36:51 -05:00
}
2026-01-07 22:55:13 -05:00
showTerminalNotification ( ` Deleted ${ deleted } execution(s) ` , 'success' ) ;
2026-01-07 22:36:51 -05:00
refreshData ( ) ;
} catch ( error ) {
console . error ( 'Error clearing executions:' , error ) ;
2026-01-07 22:55:13 -05:00
showTerminalNotification ( 'Error clearing executions' , 'error' ) ;
2026-01-07 22:36:51 -05:00
}
}
2026-03-03 16:20:05 -05:00
// ── Workflow execution with optional param modal ───────────────────
let _pendingExecWorkflowId = null ;
async function executeWorkflow ( workflowId ) {
const def = _workflowRegistry [ workflowId ] || { } ;
const paramDefs = def . params || [ ] ;
if ( paramDefs . length > 0 ) {
showParamModal ( workflowId , paramDefs ) ;
} else {
const name = document . querySelector ( ` [onclick="executeWorkflow(' ${ workflowId } ')"] ` )
? . closest ( '.workflow-item' ) ? . querySelector ( '.workflow-name' ) ? . textContent || 'this workflow' ;
2026-03-11 23:06:09 -04:00
const choice = confirm ( ` Execute: ${ name } ? \n \n Click OK to run normally, or Cancel to abort. \n (Use the workflow's Run button with dry-run checkbox for a dry run.) ` ) ;
if ( ! choice ) return ;
await startExecution ( workflowId , { } , false ) ;
2026-03-03 16:20:05 -05:00
}
}
2026-01-07 22:36:51 -05:00
2026-03-11 23:06:09 -04:00
async function startExecution ( workflowId , params , dryRun = false ) {
2025-11-29 19:26:20 -05:00
try {
const response = await fetch ( '/api/executions' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
2026-03-11 23:06:09 -04:00
body : JSON . stringify ( { workflow _id : workflowId , params , dry _run : dryRun } )
2025-11-29 19:26:20 -05:00
} ) ;
if ( response . ok ) {
2025-11-30 13:03:18 -05:00
switchTab ( 'executions' ) ;
refreshData ( ) ;
2025-11-29 19:26:20 -05:00
} else {
2026-03-03 16:20:05 -05:00
const err = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
alert ( 'Failed to start: ' + ( err . error || response . status ) ) ;
2025-11-29 19:26:20 -05:00
}
} catch ( error ) {
2026-03-03 16:20:05 -05:00
alert ( 'Error starting workflow: ' + error . message ) ;
}
}
function showParamModal ( workflowId , paramDefs ) {
_pendingExecWorkflowId = workflowId ;
const form = document . getElementById ( 'paramModalForm' ) ;
form . innerHTML = paramDefs . map ( p => `
<div style="margin-bottom:14px;">
<label style="display:block;margin-bottom:4px;color:var(--terminal-amber);font-size:.85em;">
${ p . label || p . name } ${ p . required ? ' <span style="color:var(--terminal-red)">*</span>' : '' }
</label>
<input type="text" id="param_ ${ p . name } "
placeholder=" ${ p . placeholder || '' } "
style="width:100%;background:#0a0a0a;border:1px solid var(--terminal-green);color:var(--terminal-green);font-family:var(--font-mono);padding:6px 8px;font-size:.9em;"
${ p . required ? 'required' : '' } >
</div> ` ) . join ( '' ) ;
document . getElementById ( 'paramModal' ) . style . display = 'flex' ;
2026-03-11 22:53:25 -04:00
// Focus first input; Enter key submits — use single delegated listener to avoid duplicates
if ( form . _keydownHandler ) form . removeEventListener ( 'keydown' , form . _keydownHandler ) ;
form . _keydownHandler = ( e ) => { if ( e . key === 'Enter' && e . target . tagName === 'INPUT' ) submitParamForm ( ) ; } ;
form . addEventListener ( 'keydown' , form . _keydownHandler ) ;
2026-03-03 16:20:05 -05:00
const first = form . querySelector ( 'input' ) ;
if ( first ) setTimeout ( ( ) => first . focus ( ) , 50 ) ;
}
function closeParamModal ( ) {
document . getElementById ( 'paramModal' ) . style . display = 'none' ;
2026-03-11 23:06:09 -04:00
document . getElementById ( 'paramDryRun' ) . checked = false ;
2026-03-03 16:20:05 -05:00
_pendingExecWorkflowId = null ;
}
async function submitParamForm ( ) {
if ( ! _pendingExecWorkflowId ) return ;
const def = _workflowRegistry [ _pendingExecWorkflowId ] || { } ;
const paramDefs = def . params || [ ] ;
const params = { } ;
for ( const p of paramDefs ) {
const el = document . getElementById ( ` param_ ${ p . name } ` ) ;
const val = el ? el . value . trim ( ) : '' ;
if ( p . required && ! val ) {
el . style . borderColor = 'var(--terminal-red)' ;
el . focus ( ) ;
return ;
}
if ( val ) params [ p . name ] = val ;
2025-11-29 19:26:20 -05:00
}
2026-03-11 23:06:09 -04:00
const dryRun = document . getElementById ( 'paramDryRun' ) . checked ;
const wfId = _pendingExecWorkflowId ;
2026-03-03 16:20:05 -05:00
closeParamModal ( ) ;
2026-03-11 23:06:09 -04:00
await startExecution ( wfId , params , dryRun ) ;
2025-11-29 19:26:20 -05:00
}
2025-11-30 13:03:18 -05:00
async function viewExecution ( executionId ) {
try {
const response = await fetch ( ` /api/executions/ ${ executionId } ` ) ;
const execution = await response . json ( ) ;
let html = `
<div><strong>Status:</strong> <span class="status ${ execution . status } "> ${ execution . status } </span></div>
<div><strong>Started:</strong> ${ new Date ( execution . started _at ) . toLocaleString ( ) } </div>
${ execution . completed _at ? ` <div><strong>Completed:</strong> ${ new Date ( execution . completed _at ) . toLocaleString ( ) } </div> ` : '' }
<div><strong>Started by:</strong> ${ execution . started _by } </div>
` ;
if ( execution . waiting _for _input && execution . prompt ) {
html += `
<div class="prompt-box">
2026-03-03 16:55:02 -05:00
<h3>Waiting for Input</h3>
<p> ${ escapeHtml ( execution . prompt . message || '' ) } </p>
<div style="margin-top: 10px;">
${ ( execution . prompt . options || [ ] ) . map ( opt =>
` <button class="prompt-opt-btn" onclick="respondToPrompt(' ${ executionId } ', ${ JSON . stringify ( opt ) } )"> ${ escapeHtml ( opt ) } </button> `
2025-11-30 13:03:18 -05:00
) . join ( '' ) }
</div>
</div>
` ;
}
2026-03-03 16:55:02 -05:00
2025-11-30 13:03:18 -05:00
if ( execution . logs && execution . logs . length > 0 ) {
html += '<h3 style="margin-top: 20px; margin-bottom: 10px;">Execution Logs:</h3>' ;
2026-03-03 16:55:02 -05:00
// Pass executionId only to prompt steps that are still pending (no response after them)
const logs = execution . logs ;
logs . forEach ( ( log , idx ) => {
let promptExecId = null ;
if ( log . action === 'prompt' && execution . waiting _for _input ) {
// Only make interactive if this is the last prompt and no response follows
const hasResponse = logs . slice ( idx + 1 ) . some ( l => l . action === 'prompt_response' ) ;
if ( ! hasResponse ) promptExecId = executionId ;
}
html += formatLogEntry ( log , promptExecId ) ;
2025-11-30 13:03:18 -05:00
} ) ;
}
2026-01-07 22:49:20 -05:00
// Add action buttons
html += '<div style="margin-top: 20px; padding-top: 15px; border-top: 1px solid var(--terminal-green); display: flex; gap: 10px;">' ;
2026-01-08 22:11:59 -05:00
// Abort button (only for running executions)
if ( execution . status === 'running' ) {
html += ` <button onclick="abortExecution(' ${ executionId } ')" style="background-color: var(--terminal-red); border-color: var(--terminal-red);">[ ⛔ Abort Execution ]</button> ` ;
}
2026-01-07 22:49:20 -05:00
// Re-run button (only for quick commands with command in logs)
const commandLog = execution . logs ? . find ( l => l . action === 'command_sent' ) ;
if ( commandLog && commandLog . command ) {
html += ` <button onclick="rerunCommand(' ${ escapeHtml ( commandLog . command ) } ', ' ${ commandLog . worker _id } ')">[ 🔄 Re-run Command ]</button> ` ;
}
// Download logs button
html += ` <button onclick="downloadExecutionLogs(' ${ executionId } ')">[ 💾 Download Logs ]</button> ` ;
html += '</div>' ;
2025-11-30 13:03:18 -05:00
document . getElementById ( 'executionDetails' ) . innerHTML = html ;
2026-01-07 20:20:18 -05:00
const modal = document . getElementById ( 'viewExecutionModal' ) ;
modal . dataset . executionId = executionId ;
modal . classList . add ( 'show' ) ;
2025-11-30 13:03:18 -05:00
} catch ( error ) {
console . error ( 'Error viewing execution:' , error ) ;
alert ( 'Error loading execution details' ) ;
}
}
2026-03-03 16:55:02 -05:00
function formatLogEntry ( log , executionId = null ) {
2026-01-07 22:41:29 -05:00
const timestamp = new Date ( log . timestamp ) . toLocaleTimeString ( ) ;
// Format based on log action type
if ( log . action === 'command_sent' ) {
return `
<div class="log-entry log-command-sent">
<div class="log-timestamp">[ ${ timestamp } ]</div>
<div class="log-title">Command Sent</div>
<div class="log-details">
<div class="log-field"><span class="log-label">Command:</span> <code> ${ escapeHtml ( log . command ) } </code></div>
</div>
</div>
` ;
}
if ( log . action === 'command_result' ) {
const statusIcon = log . success ? '✓' : '✗' ;
const statusClass = log . success ? 'success' : 'failed' ;
return `
<div class="log-entry log-command-result ${ statusClass } ">
<div class="log-timestamp">[ ${ timestamp } ]</div>
<div class="log-title"> ${ statusIcon } Command Result</div>
<div class="log-details">
<div class="log-field"><span class="log-label">Status:</span> ${ log . success ? 'Success' : 'Failed' } </div>
${ log . duration ? ` <div class="log-field"><span class="log-label">Duration:</span> ${ log . duration } ms</div> ` : '' }
${ log . stdout ? ` <div class="log-field"><span class="log-label">Output:</span><pre class="log-output"> ${ escapeHtml ( log . stdout ) } </pre></div> ` : '' }
${ log . stderr ? ` <div class="log-field"><span class="log-label">Errors:</span><pre class="log-output log-error"> ${ escapeHtml ( log . stderr ) } </pre></div> ` : '' }
${ log . error ? ` <div class="log-field"><span class="log-label">Error:</span> ${ escapeHtml ( log . error ) } </div> ` : '' }
</div>
</div>
` ;
}
2026-01-07 23:19:12 -05:00
// Workflow step logs
if ( log . action === 'step_started' ) {
return `
<div class="log-entry" style="border-left-color: var(--terminal-amber);">
<div class="log-timestamp">[ ${ timestamp } ]</div>
2026-03-11 22:53:25 -04:00
<div class="log-title" style="color: var(--terminal-amber);">▶️ Step ${ log . step } : ${ escapeHtml ( log . step _name || '' ) } </div>
2026-01-07 23:19:12 -05:00
</div>
` ;
}
if ( log . action === 'step_completed' ) {
return `
<div class="log-entry" style="border-left-color: var(--terminal-green);">
<div class="log-timestamp">[ ${ timestamp } ]</div>
2026-03-11 22:53:25 -04:00
<div class="log-title" style="color: var(--terminal-green);">✓ Step ${ log . step } Completed: ${ escapeHtml ( log . step _name || '' ) } </div>
2026-01-07 23:19:12 -05:00
</div>
` ;
}
if ( log . action === 'waiting' ) {
return `
<div class="log-entry" style="border-left-color: var(--terminal-amber);">
<div class="log-timestamp">[ ${ timestamp } ]</div>
2026-03-11 22:53:25 -04:00
<div class="log-title" style="color: var(--terminal-amber);">⏳ Waiting ${ escapeHtml ( String ( log . duration || 0 ) ) } seconds...</div>
2026-01-07 23:19:12 -05:00
</div>
` ;
}
if ( log . action === 'no_workers' ) {
return `
<div class="log-entry failed">
<div class="log-timestamp">[ ${ timestamp } ]</div>
<div class="log-title">✗ Step ${ log . step } : No Workers Available</div>
<div class="log-details">
<div class="log-field"> ${ escapeHtml ( log . message ) } </div>
</div>
</div>
` ;
}
if ( log . action === 'worker_offline' ) {
return `
<div class="log-entry" style="border-left-color: #ff4444;">
<div class="log-timestamp">[ ${ timestamp } ]</div>
<div class="log-title" style="color: #ff4444;">⚠️ Worker Offline</div>
<div class="log-details">
2026-03-11 22:53:25 -04:00
<div class="log-field"><span class="log-label">Worker ID:</span> ${ escapeHtml ( log . worker _id || '' ) } </div>
2026-01-07 23:19:12 -05:00
</div>
</div>
` ;
}
if ( log . action === 'workflow_error' ) {
return `
<div class="log-entry failed">
<div class="log-timestamp">[ ${ timestamp } ]</div>
<div class="log-title">✗ Workflow Error</div>
<div class="log-details">
<div class="log-field"><span class="log-label">Error:</span> ${ escapeHtml ( log . error ) } </div>
</div>
</div>
` ;
}
2026-01-08 22:11:59 -05:00
if ( log . action === 'execution_aborted' ) {
return `
<div class="log-entry" style="border-left-color: var(--terminal-red);">
<div class="log-timestamp">[ ${ timestamp } ]</div>
<div class="log-title" style="color: var(--terminal-red);">⛔ Execution Aborted</div>
<div class="log-details">
<div class="log-field"><span class="log-label">Aborted by:</span> ${ escapeHtml ( log . aborted _by ) } </div>
</div>
</div>
` ;
}
2026-03-03 16:55:02 -05:00
if ( log . action === 'prompt' ) {
const optionsHtml = ( log . options || [ ] ) . map ( opt => {
if ( executionId ) {
return ` <button class="prompt-opt-btn" onclick="respondToPrompt(' ${ executionId } ', ${ JSON . stringify ( opt ) } )"> ${ escapeHtml ( opt ) } </button> ` ;
}
return ` <button class="prompt-opt-btn answered" disabled> ${ escapeHtml ( opt ) } </button> ` ;
} ) . join ( '' ) ;
return `
<div class="log-entry" style="border-left-color: var(--terminal-cyan);">
<div class="log-timestamp">[ ${ timestamp } ]</div>
<div class="log-title" style="color: var(--terminal-cyan);">❓ Step ${ log . step } : ${ escapeHtml ( log . step _name || 'Prompt' ) } </div>
<div class="log-details">
<div style="color: var(--terminal-green); margin-bottom: 10px;"> ${ escapeHtml ( log . message || '' ) } </div>
<div> ${ optionsHtml } </div>
</div>
</div>
` ;
}
if ( log . action === 'prompt_response' ) {
return `
<div class="log-entry" style="border-left-color: var(--terminal-green);">
<div class="log-timestamp">[ ${ timestamp } ]</div>
<div class="log-title" style="color: var(--terminal-green);">↪ Response: <strong style="color: var(--terminal-amber);"> ${ escapeHtml ( log . response || '' ) } </strong> ${ log . responded _by ? ` <span style="color:#666;font-size:.85em;margin-left:10px;">by ${ escapeHtml ( log . responded _by ) } </span> ` : '' } </div>
</div>
` ;
}
if ( log . action === 'step_skipped' ) {
return `
<div class="log-entry" style="border-left-color: #555;">
<div class="log-timestamp">[ ${ timestamp } ]</div>
<div class="log-title" style="color: #666;">⊘ Step ${ log . step } Skipped ${ log . reason ? ': ' + escapeHtml ( log . reason ) : '' } </div>
</div>
` ;
}
2026-01-07 22:41:29 -05:00
// Fallback for unknown log types
return ` <div class="log-entry"><pre> ${ JSON . stringify ( log , null , 2 ) } </pre></div> ` ;
}
function escapeHtml ( text ) {
const div = document . createElement ( 'div' ) ;
div . textContent = text ;
return div . innerHTML ;
}
2026-01-07 22:43:13 -05:00
function formatBytes ( bytes ) {
if ( ! bytes || bytes === 0 ) return '0 B' ;
const k = 1024 ;
const sizes = [ 'B' , 'KB' , 'MB' , 'GB' , 'TB' ] ;
const i = Math . floor ( Math . log ( bytes ) / Math . log ( k ) ) ;
return ( bytes / Math . pow ( k , i ) ) . toFixed ( 1 ) + ' ' + sizes [ i ] ;
}
function formatUptime ( seconds ) {
if ( ! seconds ) return 'N/A' ;
const days = Math . floor ( seconds / 86400 ) ;
const hours = Math . floor ( ( seconds % 86400 ) / 3600 ) ;
const minutes = Math . floor ( ( seconds % 3600 ) / 60 ) ;
if ( days > 0 ) return ` ${ days } d ${ hours } h ${ minutes } m ` ;
if ( hours > 0 ) return ` ${ hours } h ${ minutes } m ` ;
return ` ${ minutes } m ` ;
}
function getTimeAgo ( date ) {
const seconds = Math . floor ( ( new Date ( ) - date ) / 1000 ) ;
if ( seconds < 60 ) return ` ${ seconds } s ago ` ;
const minutes = Math . floor ( seconds / 60 ) ;
if ( minutes < 60 ) return ` ${ minutes } m ago ` ;
const hours = Math . floor ( minutes / 60 ) ;
if ( hours < 24 ) return ` ${ hours } h ago ` ;
const days = Math . floor ( hours / 24 ) ;
return ` ${ days } d ago ` ;
}
2026-01-07 22:49:20 -05:00
async function rerunCommand ( command , workerId ) {
if ( ! confirm ( ` Re-run command: ${ command } ? ` ) ) return ;
closeModal ( 'viewExecutionModal' ) ;
switchTab ( 'quickcommand' ) ;
// Set the worker and command
document . getElementById ( 'quickWorkerSelect' ) . value = workerId ;
document . getElementById ( 'quickCommand' ) . value = command ;
// Scroll to the command field
document . getElementById ( 'quickCommand' ) . scrollIntoView ( { behavior : 'smooth' } ) ;
}
2026-01-08 22:11:59 -05:00
async function abortExecution ( executionId ) {
if ( ! confirm ( 'Are you sure you want to abort this execution? This will mark it as failed.' ) ) return ;
try {
const response = await fetch ( ` /api/executions/ ${ executionId } /abort ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' }
} ) ;
if ( response . ok ) {
showTerminalNotification ( 'Execution aborted' , 'success' ) ;
closeModal ( 'viewExecutionModal' ) ;
refreshData ( ) ;
} else {
alert ( 'Failed to abort execution' ) ;
}
} catch ( error ) {
console . error ( 'Error aborting execution:' , error ) ;
alert ( 'Error aborting execution' ) ;
}
}
2026-01-07 22:49:20 -05:00
async function downloadExecutionLogs ( executionId ) {
try {
const response = await fetch ( ` /api/executions/ ${ executionId } ` ) ;
const execution = await response . json ( ) ;
// Create downloadable JSON
const data = {
execution _id : executionId ,
workflow _name : execution . workflow _name || '[Quick Command]' ,
status : execution . status ,
started _by : execution . started _by ,
started _at : execution . started _at ,
completed _at : execution . completed _at ,
logs : execution . logs
} ;
// Create blob and download
const blob = new Blob ( [ JSON . stringify ( data , null , 2 ) ] , { type : 'application/json' } ) ;
const url = URL . createObjectURL ( blob ) ;
const a = document . createElement ( 'a' ) ;
a . href = url ;
a . download = ` execution- ${ executionId } - ${ new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] } .json ` ;
document . body . appendChild ( a ) ;
a . click ( ) ;
document . body . removeChild ( a ) ;
URL . revokeObjectURL ( url ) ;
} catch ( error ) {
console . error ( 'Error downloading logs:' , error ) ;
alert ( 'Error downloading execution logs' ) ;
}
}
2025-11-30 13:03:18 -05:00
async function respondToPrompt ( executionId , response ) {
try {
const res = await fetch ( ` /api/executions/ ${ executionId } /respond ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { response } )
} ) ;
2026-03-03 16:55:02 -05:00
2025-11-30 13:03:18 -05:00
if ( res . ok ) {
2026-03-03 16:55:02 -05:00
showTerminalNotification ( ` Response submitted: ${ response } ` , 'success' ) ;
// Refresh the modal to show the next step
viewExecution ( executionId ) ;
2025-11-30 13:03:18 -05:00
} else {
2026-03-03 16:55:02 -05:00
const data = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
showTerminalNotification ( data . error || 'Failed to submit response' , 'error' ) ;
2025-11-30 13:03:18 -05:00
}
} catch ( error ) {
console . error ( 'Error responding to prompt:' , error ) ;
2026-03-03 16:55:02 -05:00
showTerminalNotification ( 'Error submitting response' , 'error' ) ;
2025-11-30 13:03:18 -05:00
}
}
2026-01-07 22:45:40 -05:00
// Command Templates
const commandTemplates = [
{ name : 'System Info' , cmd : 'uname -a' , desc : 'Show system information' } ,
{ name : 'Uptime' , cmd : 'uptime' , desc : 'Show system uptime and load' } ,
{ name : 'Disk Usage' , cmd : 'df -h' , desc : 'Show disk space usage' } ,
{ name : 'Memory Usage' , cmd : 'free -h' , desc : 'Show memory usage' } ,
{ name : 'CPU Info' , cmd : 'lscpu' , desc : 'Show CPU information' } ,
{ name : 'Running Processes' , cmd : 'ps aux --sort=-%mem | head -20' , desc : 'Top 20 processes by memory' } ,
{ name : 'Network Interfaces' , cmd : 'ip addr show' , desc : 'Show network interfaces' } ,
{ name : 'Active Connections' , cmd : 'ss -tunap' , desc : 'Show active network connections' } ,
{ name : 'Docker Containers' , cmd : 'docker ps -a' , desc : 'List all Docker containers' } ,
{ name : 'System Log Tail' , cmd : 'tail -n 50 /var/log/syslog' , desc : 'Last 50 lines of system log' } ,
{ name : 'Who is Logged In' , cmd : 'w' , desc : 'Show logged in users' } ,
{ name : 'Last Logins' , cmd : 'last -n 20' , desc : 'Show last 20 logins' }
] ;
function showCommandTemplates ( ) {
const html = commandTemplates . map ( ( template , index ) => `
<div class="template-item" onclick="useTemplate( ${ index } )" style="cursor: pointer; padding: 12px; margin: 8px 0; background: #001a00; border: 1px solid #003300; border-left: 3px solid var(--terminal-green);">
<div style="color: var(--terminal-green); font-weight: bold; margin-bottom: 4px;"> ${ template . name } </div>
<div style="color: var(--terminal-amber); font-family: var(--font-mono); font-size: 0.9em; margin-bottom: 4px;"><code> ${ escapeHtml ( template . cmd ) } </code></div>
<div style="color: #666; font-size: 0.85em;"> ${ template . desc } </div>
</div>
` ) . join ( '' ) ;
document . getElementById ( 'templateList' ) . innerHTML = html ;
document . getElementById ( 'commandTemplatesModal' ) . classList . add ( 'show' ) ;
}
function useTemplate ( index ) {
document . getElementById ( 'quickCommand' ) . value = commandTemplates [ index ] . cmd ;
closeModal ( 'commandTemplatesModal' ) ;
}
function showCommandHistory ( ) {
const history = JSON . parse ( localStorage . getItem ( 'commandHistory' ) || '[]' ) ;
if ( history . length === 0 ) {
document . getElementById ( 'historyList' ) . innerHTML = '<div class="empty">No command history yet</div>' ;
} else {
const html = history . map ( ( item , index ) => `
<div class="history-item" onclick="useHistoryCommand( ${ index } )" style="cursor: pointer; padding: 12px; margin: 8px 0; background: #001a00; border: 1px solid #003300; border-left: 3px solid var(--terminal-amber);">
<div style="color: var(--terminal-green); font-family: var(--font-mono); margin-bottom: 4px;"><code> ${ escapeHtml ( item . command ) } </code></div>
<div style="color: #666; font-size: 0.85em;"> ${ new Date ( item . timestamp ) . toLocaleString ( ) } - ${ item . worker } </div>
</div>
` ) . join ( '' ) ;
document . getElementById ( 'historyList' ) . innerHTML = html ;
}
document . getElementById ( 'commandHistoryModal' ) . classList . add ( 'show' ) ;
}
function useHistoryCommand ( index ) {
const history = JSON . parse ( localStorage . getItem ( 'commandHistory' ) || '[]' ) ;
document . getElementById ( 'quickCommand' ) . value = history [ index ] . command ;
closeModal ( 'commandHistoryModal' ) ;
}
function addToCommandHistory ( command , workerName ) {
const history = JSON . parse ( localStorage . getItem ( 'commandHistory' ) || '[]' ) ;
// Add to beginning, limit to 50 items
history . unshift ( {
command : command ,
worker : workerName ,
timestamp : new Date ( ) . toISOString ( )
} ) ;
// Keep only last 50 commands
if ( history . length > 50 ) {
history . splice ( 50 ) ;
}
localStorage . setItem ( 'commandHistory' , JSON . stringify ( history ) ) ;
}
2026-01-07 23:03:45 -05:00
function toggleWorkerSelection ( ) {
const mode = document . querySelector ( 'input[name="execMode"]:checked' ) . value ;
const singleMode = document . getElementById ( 'singleWorkerMode' ) ;
const multiMode = document . getElementById ( 'multiWorkerMode' ) ;
if ( mode === 'single' ) {
singleMode . style . display = 'block' ;
multiMode . style . display = 'none' ;
} else {
singleMode . style . display = 'none' ;
multiMode . style . display = 'block' ;
}
}
function selectAllWorkers ( ) {
document . querySelectorAll ( 'input[name="workerCheckbox"]' ) . forEach ( cb => {
cb . checked = true ;
} ) ;
}
function selectOnlineWorkers ( ) {
document . querySelectorAll ( 'input[name="workerCheckbox"]' ) . forEach ( cb => {
cb . checked = cb . getAttribute ( 'data-status' ) === 'online' ;
} ) ;
}
function deselectAllWorkers ( ) {
document . querySelectorAll ( 'input[name="workerCheckbox"]' ) . forEach ( cb => {
cb . checked = false ;
} ) ;
}
2025-11-30 13:03:18 -05:00
async function deleteWorker ( workerId , name ) {
if ( ! confirm ( ` Delete worker: ${ name } ? ` ) ) return ;
try {
const response = await fetch ( ` /api/workers/ ${ workerId } ` , {
method : 'DELETE'
} ) ;
if ( response . ok ) {
alert ( 'Worker deleted' ) ;
refreshData ( ) ;
} else {
const data = await response . json ( ) ;
alert ( data . error || 'Failed to delete worker' ) ;
}
} catch ( error ) {
console . error ( 'Error deleting worker:' , error ) ;
alert ( 'Error deleting worker' ) ;
}
}
2025-11-29 19:26:20 -05:00
async function deleteWorkflow ( workflowId , name ) {
2025-11-30 13:03:18 -05:00
if ( ! confirm ( ` Delete workflow: ${ name } ? This cannot be undone. ` ) ) return ;
2025-11-29 19:26:20 -05:00
try {
const response = await fetch ( ` /api/workflows/ ${ workflowId } ` , {
method : 'DELETE'
} ) ;
if ( response . ok ) {
alert ( 'Workflow deleted' ) ;
2025-11-30 13:03:18 -05:00
refreshData ( ) ;
2025-11-29 19:26:20 -05:00
} else {
const data = await response . json ( ) ;
alert ( data . error || 'Failed to delete workflow' ) ;
}
} catch ( error ) {
console . error ( 'Error deleting workflow:' , error ) ;
alert ( 'Error deleting workflow' ) ;
}
}
2026-03-03 16:55:02 -05:00
async function editWorkflow ( workflowId ) {
try {
const response = await fetch ( ` /api/workflows/ ${ workflowId } ` ) ;
if ( ! response . ok ) throw new Error ( 'Workflow not found' ) ;
const wf = await response . json ( ) ;
document . getElementById ( 'editWorkflowId' ) . value = wf . id ;
document . getElementById ( 'editWorkflowName' ) . value = wf . name ;
document . getElementById ( 'editWorkflowDescription' ) . value = wf . description || '' ;
document . getElementById ( 'editWorkflowDefinition' ) . value = JSON . stringify ( wf . definition , null , 2 ) ;
2026-03-11 23:06:09 -04:00
document . getElementById ( 'editWorkflowWebhookUrl' ) . value = wf . webhook _url || '' ;
2026-03-03 16:55:02 -05:00
document . getElementById ( 'editWorkflowError' ) . style . display = 'none' ;
document . getElementById ( 'editWorkflowModal' ) . classList . add ( 'show' ) ;
} catch ( error ) {
console . error ( 'Error loading workflow for edit:' , error ) ;
showTerminalNotification ( 'Error loading workflow' , 'error' ) ;
}
}
async function saveWorkflow ( ) {
const id = document . getElementById ( 'editWorkflowId' ) . value ;
const name = document . getElementById ( 'editWorkflowName' ) . value . trim ( ) ;
const description = document . getElementById ( 'editWorkflowDescription' ) . value . trim ( ) ;
const definitionText = document . getElementById ( 'editWorkflowDefinition' ) . value ;
2026-03-11 23:06:09 -04:00
const webhook _url = document . getElementById ( 'editWorkflowWebhookUrl' ) . value . trim ( ) || null ;
2026-03-03 16:55:02 -05:00
const errorEl = document . getElementById ( 'editWorkflowError' ) ;
if ( ! name ) {
errorEl . textContent = 'Name is required' ;
errorEl . style . display = 'block' ;
return ;
}
let definition ;
try {
definition = JSON . parse ( definitionText ) ;
} catch ( e ) {
errorEl . textContent = 'Invalid JSON: ' + e . message ;
errorEl . style . display = 'block' ;
return ;
}
errorEl . style . display = 'none' ;
try {
const response = await fetch ( ` /api/workflows/ ${ id } ` , {
method : 'PUT' ,
headers : { 'Content-Type' : 'application/json' } ,
2026-03-11 23:06:09 -04:00
body : JSON . stringify ( { name , description , definition , webhook _url } )
2026-03-03 16:55:02 -05:00
} ) ;
if ( response . ok ) {
closeModal ( 'editWorkflowModal' ) ;
showTerminalNotification ( 'Workflow saved!' , 'success' ) ;
loadWorkflows ( ) ;
} else {
const data = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
errorEl . textContent = data . error || 'Failed to save workflow' ;
errorEl . style . display = 'block' ;
}
} catch ( error ) {
errorEl . textContent = 'Error saving workflow: ' + error . message ;
errorEl . style . display = 'block' ;
}
}
2025-11-30 13:03:18 -05:00
function showCreateWorkflow ( ) {
document . getElementById ( 'createWorkflowModal' ) . classList . add ( 'show' ) ;
}
async function createWorkflow ( ) {
const name = document . getElementById ( 'workflowName' ) . value ;
const description = document . getElementById ( 'workflowDescription' ) . value ;
const definitionText = document . getElementById ( 'workflowDefinition' ) . value ;
2026-03-11 23:06:09 -04:00
const webhook _url = document . getElementById ( 'workflowWebhookUrl' ) . value . trim ( ) || null ;
2025-11-30 13:03:18 -05:00
if ( ! name || ! definitionText ) {
alert ( 'Name and definition are required' ) ;
return ;
}
2026-03-11 23:06:09 -04:00
2026-01-07 23:27:54 -05:00
let definition ;
try {
definition = JSON . parse ( definitionText ) ;
} catch ( error ) {
alert ( 'Invalid JSON definition: ' + error . message ) ;
return ;
}
2025-11-30 13:03:18 -05:00
try {
const response = await fetch ( '/api/workflows' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
2026-03-11 23:06:09 -04:00
body : JSON . stringify ( { name , description , definition , webhook _url } )
2025-11-30 13:03:18 -05:00
} ) ;
2026-01-07 23:27:54 -05:00
2025-11-30 13:03:18 -05:00
if ( response . ok ) {
closeModal ( 'createWorkflowModal' ) ;
switchTab ( 'workflows' ) ;
2026-01-07 23:27:54 -05:00
showTerminalNotification ( 'Workflow created successfully!' , 'success' ) ;
2025-11-30 13:03:18 -05:00
refreshData ( ) ;
} else {
alert ( 'Failed to create workflow' ) ;
}
} catch ( error ) {
2026-01-07 23:27:54 -05:00
alert ( 'Error creating workflow: ' + error . message ) ;
2025-11-30 13:03:18 -05:00
}
}
async function executeQuickCommand ( ) {
const command = document . getElementById ( 'quickCommand' ) . value ;
2026-01-07 23:03:45 -05:00
const execMode = document . querySelector ( 'input[name="execMode"]:checked' ) . value ;
2026-01-07 22:45:40 -05:00
2026-01-07 23:03:45 -05:00
if ( ! command ) {
alert ( 'Please enter a command' ) ;
2025-11-30 13:03:18 -05:00
return ;
}
2026-01-07 22:45:40 -05:00
2025-11-30 13:03:18 -05:00
const resultDiv = document . getElementById ( 'quickCommandResult' ) ;
2026-01-07 22:45:40 -05:00
2026-01-07 23:03:45 -05:00
if ( execMode === 'single' ) {
// Single worker execution
const workerId = document . getElementById ( 'quickWorkerSelect' ) . value ;
2026-01-07 22:45:40 -05:00
2026-01-07 23:03:45 -05:00
if ( ! workerId ) {
alert ( 'Please select a worker' ) ;
return ;
}
2026-01-07 22:45:40 -05:00
2026-01-07 23:03:45 -05:00
const worker = workers . find ( w => w . id === workerId ) ;
const workerName = worker ? worker . name : 'Unknown' ;
2026-01-07 22:45:40 -05:00
2026-01-07 23:03:45 -05:00
resultDiv . innerHTML = '<div class="loading">Executing command...</div>' ;
try {
const response = await fetch ( ` /api/workers/ ${ workerId } /command ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { command } )
} ) ;
if ( response . ok ) {
const data = await response . json ( ) ;
addToCommandHistory ( command , workerName ) ;
resultDiv . innerHTML = `
<div style="background: #001a00; border: 2px solid var(--terminal-green); padding: 15px;">
<strong style="color: var(--terminal-green);">✓ Command sent successfully!</strong>
<div style="margin-top: 10px; font-family: var(--font-mono); font-size: 0.9em; color: var(--terminal-green);">
2026-03-11 22:53:25 -04:00
Execution ID: ${ escapeHtml ( String ( data . execution _id || '' ) ) }
2026-01-07 23:03:45 -05:00
</div>
<div style="margin-top: 10px; color: var(--terminal-amber);">
Check the Executions tab to see the results
</div>
2025-11-30 13:03:18 -05:00
</div>
2026-01-07 23:03:45 -05:00
` ;
terminalBeep ( 'success' ) ;
} else {
resultDiv . innerHTML = '<div style="color: #ef4444;">Failed to execute command</div>' ;
terminalBeep ( 'error' ) ;
}
} catch ( error ) {
console . error ( 'Error executing command:' , error ) ;
resultDiv . innerHTML = '<div style="color: #ef4444;">Error: ' + error . message + '</div>' ;
terminalBeep ( 'error' ) ;
}
} else {
// Multi-worker execution
const selectedCheckboxes = document . querySelectorAll ( 'input[name="workerCheckbox"]:checked' ) ;
const selectedWorkerIds = Array . from ( selectedCheckboxes ) . map ( cb => cb . value ) ;
if ( selectedWorkerIds . length === 0 ) {
alert ( 'Please select at least one worker' ) ;
return ;
}
resultDiv . innerHTML = ` <div class="loading">Executing command on ${ selectedWorkerIds . length } worker(s)...</div> ` ;
const results = [ ] ;
let successCount = 0 ;
let failCount = 0 ;
for ( const workerId of selectedWorkerIds ) {
try {
const worker = workers . find ( w => w . id === workerId ) ;
const response = await fetch ( ` /api/workers/ ${ workerId } /command ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { command } )
} ) ;
if ( response . ok ) {
const data = await response . json ( ) ;
results . push ( {
worker : worker . name ,
success : true ,
executionId : data . execution _id
} ) ;
successCount ++ ;
} else {
results . push ( {
worker : worker . name ,
success : false ,
error : 'Failed to execute'
} ) ;
failCount ++ ;
}
} catch ( error ) {
const worker = workers . find ( w => w . id === workerId ) ;
results . push ( {
worker : worker ? worker . name : workerId ,
success : false ,
error : error . message
} ) ;
failCount ++ ;
}
}
// Add to history with multi-worker notation
addToCommandHistory ( command , ` ${ selectedWorkerIds . length } workers ` ) ;
// Display results summary
resultDiv . innerHTML = `
<div style="background: #001a00; border: 2px solid var(--terminal-green); padding: 15px;">
<strong style="color: var(--terminal-amber);">Multi-Worker Execution Complete</strong>
<div style="margin-top: 10px; font-family: var(--font-mono); font-size: 0.9em;">
<span style="color: var(--terminal-green);">✓ Success: ${ successCount } </span> |
<span style="color: #ef4444;">✗ Failed: ${ failCount } </span>
2025-11-30 13:03:18 -05:00
</div>
2026-01-07 23:03:45 -05:00
<div style="margin-top: 15px; max-height: 300px; overflow-y: auto;">
${ results . map ( r => `
<div style="margin-bottom: 8px; padding: 8px; border-left: 3px solid ${ r . success ? 'var(--terminal-green)' : '#ef4444' } ; background: rgba(0, 0, 0, 0.5);">
<strong> ${ r . worker } </strong>:
${ r . success ?
` <span style="color: var(--terminal-green);">✓ Sent (ID: ${ r . executionId . substring ( 0 , 8 ) } ...)</span> ` :
` <span style="color: #ef4444;">✗ ${ r . error } </span> `
}
</div>
` ) . join ( '' ) }
</div>
<div style="margin-top: 15px; color: var(--terminal-amber);">
Check the Executions tab to see detailed results
</div>
</div>
` ;
if ( failCount === 0 ) {
terminalBeep ( 'success' ) ;
} else if ( successCount > 0 ) {
terminalBeep ( 'info' ) ;
2025-11-30 13:03:18 -05:00
} else {
2026-01-07 23:03:45 -05:00
terminalBeep ( 'error' ) ;
2025-11-30 13:03:18 -05:00
}
}
}
function closeModal ( modalId ) {
document . getElementById ( modalId ) . classList . remove ( 'show' ) ;
}
function switchTab ( tabName ) {
document . querySelectorAll ( '.tab' ) . forEach ( t => t . classList . remove ( 'active' ) ) ;
document . querySelectorAll ( '.tab-content' ) . forEach ( c => c . classList . remove ( 'active' ) ) ;
2026-03-11 22:53:25 -04:00
// Find the button by its onclick attribute rather than relying on bare `event`
const tabBtn = document . querySelector ( ` .tab[onclick*="' ${ tabName } '"] ` ) ;
if ( tabBtn ) tabBtn . classList . add ( 'active' ) ;
const tabContent = document . getElementById ( tabName ) ;
if ( tabContent ) tabContent . classList . add ( 'active' ) ;
2025-11-30 13:03:18 -05:00
}
2026-01-07 23:27:54 -05:00
async function refreshData ( ) {
try {
await loadWorkers ( ) ;
} catch ( e ) {
console . error ( 'Error loading workers:' , e ) ;
}
try {
await loadWorkflows ( ) ;
} catch ( e ) {
console . error ( 'Error loading workflows:' , e ) ;
}
try {
await loadExecutions ( ) ;
} catch ( e ) {
console . error ( 'Error loading executions:' , e ) ;
}
try {
await loadSchedules ( ) ;
} catch ( e ) {
console . error ( 'Error loading schedules:' , e ) ;
}
2025-11-29 19:26:20 -05:00
}
2026-01-07 22:52:51 -05:00
// Terminal beep sound (Web Audio API)
function terminalBeep ( type = 'success' ) {
try {
const audioContext = new ( window . AudioContext || window . webkitAudioContext ) ( ) ;
const oscillator = audioContext . createOscillator ( ) ;
const gainNode = audioContext . createGain ( ) ;
oscillator . connect ( gainNode ) ;
gainNode . connect ( audioContext . destination ) ;
// Different tones for different events
if ( type === 'success' ) {
oscillator . frequency . value = 800 ; // Higher pitch for success
} else if ( type === 'error' ) {
oscillator . frequency . value = 200 ; // Lower pitch for errors
} else {
oscillator . frequency . value = 440 ; // Standard A note
}
oscillator . type = 'sine' ;
gainNode . gain . setValueAtTime ( 0.1 , audioContext . currentTime ) ;
gainNode . gain . exponentialRampToValueAtTime ( 0.01 , audioContext . currentTime + 0.1 ) ;
oscillator . start ( audioContext . currentTime ) ;
oscillator . stop ( audioContext . currentTime + 0.1 ) ;
} catch ( error ) {
// Silently fail if Web Audio API not supported
}
}
// Show terminal notification
function showTerminalNotification ( message , type = 'info' ) {
const notification = document . createElement ( 'div' ) ;
notification . style . cssText = `
position: fixed;
top: 80px;
right: 20px;
background: #001a00;
border: 2px solid var(--terminal-green);
color: var(--terminal-green);
padding: 15px 20px;
font-family: var(--font-mono);
z-index: 10000;
animation: slide-in 0.3s ease-out;
box-shadow: 0 0 20px rgba(0, 255, 65, 0.3);
` ;
if ( type === 'error' ) {
notification . style . borderColor = '#ff4444' ;
notification . style . color = '#ff4444' ;
message = '✗ ' + message ;
} else if ( type === 'success' ) {
message = '✓ ' + message ;
} else {
message = 'ℹ ' + message ;
}
notification . textContent = message ;
document . body . appendChild ( notification ) ;
// Play beep
terminalBeep ( type ) ;
// Remove after 3 seconds
setTimeout ( ( ) => {
notification . style . opacity = '0' ;
notification . style . transition = 'opacity 0.5s' ;
setTimeout ( ( ) => notification . remove ( ) , 500 ) ;
} , 3000 ) ;
}
2025-11-29 19:26:20 -05:00
function connectWebSocket ( ) {
const protocol = window . location . protocol === 'https:' ? 'wss:' : 'ws:' ;
ws = new WebSocket ( ` ${ protocol } // ${ window . location . host } ` ) ;
2026-01-07 20:20:18 -05:00
2025-11-29 19:26:20 -05:00
ws . onmessage = ( event ) => {
2026-01-07 23:33:07 -05:00
try {
const data = JSON . parse ( event . data ) ;
console . log ( 'WebSocket message:' , data ) ;
2026-01-07 20:20:18 -05:00
// Handle specific message types
if ( data . type === 'command_result' ) {
// Display command result in real-time
console . log ( ` Command result received for execution ${ data . execution _id } ` ) ;
console . log ( ` Success: ${ data . success } ` ) ;
console . log ( ` Output: ${ data . stdout } ` ) ;
if ( data . stderr ) {
console . log ( ` Error: ${ data . stderr } ` ) ;
}
2026-03-04 16:26:18 -05:00
// Show terminal notification only for manual executions
if ( ! data . is _automated ) {
if ( data . success ) {
showTerminalNotification ( 'Command completed successfully' , 'success' ) ;
} else {
showTerminalNotification ( 'Command execution failed' , 'error' ) ;
}
2026-01-07 22:52:51 -05:00
}
2026-01-07 20:20:18 -05:00
// If viewing execution details, refresh that specific execution
const executionModal = document . getElementById ( 'viewExecutionModal' ) ;
if ( executionModal && executionModal . classList . contains ( 'show' ) ) {
// Reload execution details to show new logs
const executionId = executionModal . dataset . executionId ;
if ( executionId === data . execution _id ) {
viewExecution ( executionId ) ;
}
}
// Refresh execution list to show updated status
loadExecutions ( ) ;
}
if ( data . type === 'workflow_result' ) {
console . log ( ` Workflow ${ data . status } for execution ${ data . execution _id } ` ) ;
// Refresh execution list
loadExecutions ( ) ;
// If viewing this execution, refresh details
const executionModal = document . getElementById ( 'viewExecutionModal' ) ;
if ( executionModal && executionModal . classList . contains ( 'show' ) ) {
const executionId = executionModal . dataset . executionId ;
if ( executionId === data . execution _id ) {
viewExecution ( executionId ) ;
}
}
}
if ( data . type === 'worker_update' ) {
console . log ( ` Worker ${ data . worker _id } status: ${ data . status } ` ) ;
loadWorkers ( ) ;
}
if ( data . type === 'execution_started' || data . type === 'execution_status' ) {
loadExecutions ( ) ;
}
if ( data . type === 'workflow_created' || data . type === 'workflow_deleted' ) {
loadWorkflows ( ) ;
}
2026-03-03 16:55:02 -05:00
if ( data . type === 'workflow_updated' ) {
loadWorkflows ( ) ;
}
if ( data . type === 'execution_prompt' ) {
// If this execution is currently open, refresh to show the prompt
const executionModal = document . getElementById ( 'viewExecutionModal' ) ;
if ( executionModal && executionModal . classList . contains ( 'show' ) ) {
const currentId = executionModal . dataset . executionId ;
if ( currentId === data . execution _id ) {
viewExecution ( data . execution _id ) ;
}
}
// Also update execution list so status indicators refresh
loadExecutions ( ) ;
}
2026-01-07 23:33:07 -05:00
// Generic refresh for other message types
2026-03-03 16:55:02 -05:00
if ( ! [ 'command_result' , 'workflow_result' , 'worker_update' , 'execution_started' , 'execution_status' , 'workflow_created' , 'workflow_deleted' , 'workflow_updated' , 'execution_prompt' ] . includes ( data . type ) ) {
2026-01-07 23:33:07 -05:00
refreshData ( ) ;
}
} catch ( error ) {
console . error ( 'Error handling WebSocket message:' , error ) ;
console . error ( 'Stack trace:' , error . stack ) ;
2026-01-07 20:20:18 -05:00
}
2025-11-29 19:26:20 -05:00
} ;
2026-01-07 20:20:18 -05:00
2025-11-29 19:26:20 -05:00
ws . onclose = ( ) => {
console . log ( 'WebSocket closed, reconnecting...' ) ;
setTimeout ( connectWebSocket , 5000 ) ;
} ;
2026-03-11 22:53:25 -04:00
ws . onerror = ( error ) => {
console . error ( '[WebSocket] Connection error:' , error ) ;
} ;
2025-11-29 19:26:20 -05:00
}
2026-03-11 22:53:25 -04:00
// Close any open modal on ESC key
document . addEventListener ( 'keydown' , ( e ) => {
if ( e . key === 'Escape' ) {
document . querySelectorAll ( '.modal' ) . forEach ( modal => {
if ( modal . style . display && modal . style . display !== 'none' ) {
modal . style . display = 'none' ;
}
} ) ;
}
} ) ;
2025-11-29 19:26:20 -05:00
// Initialize
loadUser ( ) . then ( ( success ) => {
if ( success ) {
2026-03-03 16:04:22 -05:00
setExecutionView ( executionView ) ;
2025-11-29 19:26:20 -05:00
refreshData ( ) ;
connectWebSocket ( ) ;
2025-11-30 13:03:18 -05:00
setInterval ( refreshData , 30000 ) ;
2025-11-29 19:26:20 -05:00
}
} ) ;
< / script >
2026-01-07 20:12:16 -05:00
<!-- Terminal Boot Sequence -->
< div id = "boot-sequence" class = "boot-overlay" style = "display: none;" >
< pre id = "boot-text" > < / pre >
< / div >
< script >
function showBootSequence ( ) {
const bootText = document . getElementById ( 'boot-text' ) ;
const bootOverlay = document . getElementById ( 'boot-sequence' ) ;
bootOverlay . style . display = 'flex' ;
const messages = [
'╔═══════════════════════════════════════╗' ,
'║ PULSE ORCHESTRATION TERMINAL v1.0 ║' ,
'║ BOOTING SYSTEM... ║' ,
'╚═══════════════════════════════════════╝' ,
'' ,
'[ OK ] Loading kernel modules...' ,
'[ OK ] Initializing workflow engine...' ,
'[ OK ] Mounting worker connections...' ,
'[ OK ] Starting WebSocket services...' ,
'[ OK ] Rendering terminal interface...' ,
'' ,
'> SYSTEM READY ✓' ,
''
] ;
let i = 0 ;
const interval = setInterval ( ( ) => {
if ( i < messages . length ) {
bootText . textContent += messages [ i ] + '\n' ;
i ++ ;
} else {
setTimeout ( ( ) => {
bootOverlay . style . opacity = '0' ;
setTimeout ( ( ) => {
bootOverlay . style . display = 'none' ;
} , 500 ) ;
} , 500 ) ;
clearInterval ( interval ) ;
}
} , 80 ) ;
}
// Run on first visit only (per session)
if ( ! sessionStorage . getItem ( 'booted' ) ) {
showBootSequence ( ) ;
sessionStorage . setItem ( 'booted' , 'true' ) ;
}
< / script >
2025-11-29 19:26:20 -05:00
< / body >
2025-11-30 13:03:18 -05:00
< / html >