1- import { Box , useInput , useApp } from 'ink' ;
1+ import { Box , useInput , useApp , useStdout } from 'ink' ;
22import { useState } from 'react' ;
33import type { RemoteExecutor } from '../../domain/remote/executor.js' ;
44import type { ShipnodeConfig , ShipnodeApp } from '../../shared/types.js' ;
@@ -8,36 +8,121 @@ import { ReleasePanel } from './panels/ReleasePanel.js';
88import { LogPanel } from './panels/LogPanel.js' ;
99import { EventsPanel } from './panels/EventsPanel.js' ;
1010import { StaticFrontendPanel } from './panels/StaticFrontendPanel.js' ;
11+ import { AccessoriesPanel } from './panels/AccessoriesPanel.js' ;
1112import { AppSelector } from './app-selector.js' ;
13+ import { HelpOverlay } from './components/HelpOverlay.js' ;
14+ import { ConfirmDialog } from './components/ConfirmDialog.js' ;
15+ import { restartProcess } from './actions.js' ;
1216import chalk from 'chalk' ;
1317import { useMonitorData } from './hooks/use-monitor-data.js' ;
1418import { useLiveLogs } from './hooks/use-live-logs.js' ;
1519import { MonitorFrame , WaitingPanel } from './layout/MonitorFrame.js' ;
1620
21+ type View = 'dashboard' | 'logs' ;
22+ type Overlay = 'none' | 'selector' | 'help' | 'confirmRestart' ;
23+
1724interface AppProps {
1825 executor : RemoteExecutor ;
1926 config : ShipnodeConfig ;
2027 app : ShipnodeApp ;
2128 apps : ShipnodeApp [ ] ;
29+ accessoryNames : string [ ] ;
2230 targetName : string ;
2331 host : string ;
2432 interval : number ;
2533}
2634
27- export function App ( { executor, config, app : initialApp , apps, targetName, host, interval } : AppProps ) {
35+ export function App ( { executor, config, app : initialApp , apps, accessoryNames , targetName, host, interval } : AppProps ) {
2836 const { exit } = useApp ( ) ;
37+ const { stdout } = useStdout ( ) ;
2938 const [ currentApp , setCurrentApp ] = useState ( initialApp ) ;
39+ const [ view , setView ] = useState < View > ( 'dashboard' ) ;
40+ const [ overlay , setOverlay ] = useState < Overlay > ( 'none' ) ;
3041 const [ liveMode , setLiveMode ] = useState ( false ) ;
31- const [ showSelector , setShowSelector ] = useState ( false ) ;
32- const monitor = useMonitorData ( executor , config , currentApp , interval ) ;
33- const liveLogs = useLiveLogs ( executor , currentApp , liveMode , interval ) ;
42+ const [ logFilter , setLogFilter ] = useState < string | null > ( null ) ;
43+ const [ selectedProcess , setSelectedProcess ] = useState ( 0 ) ;
44+ const monitor = useMonitorData ( executor , config , currentApp , interval , accessoryNames ) ;
45+ const liveActive = liveMode || view === 'logs' ;
46+ const liveLogs = useLiveLogs ( executor , currentApp , liveActive , interval , logFilter ) ;
47+
48+ const processes = monitor . snapshot ?. processes ?? [ ] ;
49+ const selectedIndex = processes . length === 0 ? 0 : Math . min ( selectedProcess , processes . length - 1 ) ;
50+ const selectedInfo = processes [ selectedIndex ] ;
51+
52+ const confirmRestart = async ( ) : Promise < void > => {
53+ setOverlay ( 'none' ) ;
54+ if ( selectedInfo === undefined ) return ;
55+ monitor . appendEvent ( chalk . yellow ( `Restarting ${ selectedInfo . pm2Name } …` ) ) ;
56+ const result = await restartProcess ( executor , selectedInfo . pm2Name ) ;
57+ if ( result . isOk ( ) ) {
58+ monitor . appendEvent ( chalk . green ( `Restarted ${ chalk . bold ( selectedInfo . pm2Name ) } ` ) ) ;
59+ } else {
60+ monitor . appendEvent ( chalk . red ( result . error . message ) ) ;
61+ }
62+ void monitor . refresh ( ) ;
63+ } ;
64+
65+ const cycleLogFilter = ( direction : 1 | - 1 ) : void => {
66+ if ( currentApp . appType === 'frontend' ) return ;
67+ const names = monitor . snapshot ?. processes . map ( ( p ) => p . pm2Name ) ?? [ ] ;
68+ if ( names . length === 0 ) return ;
69+ const cycle : Array < string | null > = [ null , ...names ] ;
70+ const index = cycle . indexOf ( logFilter ) ;
71+ const next = cycle [ ( index + direction + cycle . length ) % cycle . length ] ;
72+ setLogFilter ( next ) ;
73+ } ;
3474
3575 useInput ( ( input , key ) => {
76+ if ( overlay === 'help' ) {
77+ setOverlay ( 'none' ) ;
78+ return ;
79+ }
80+ if ( overlay !== 'none' ) return ;
81+
3682 if ( input === 'q' ) {
3783 exit ( ) ;
84+ return ;
85+ }
86+ if ( input === '?' ) {
87+ setOverlay ( 'help' ) ;
88+ return ;
89+ }
90+ if ( input === 'r' || input === 'R' ) {
91+ void monitor . refresh ( ) ;
92+ monitor . appendEvent ( chalk . green ( 'Refresh triggered' ) ) ;
93+ return ;
94+ }
95+ if ( input === 'f' || input === 'F' ) {
96+ setView ( ( v ) => ( v === 'logs' ? 'dashboard' : 'logs' ) ) ;
97+ return ;
98+ }
99+
100+ if ( view === 'logs' ) {
101+ if ( key . escape ) setView ( 'dashboard' ) ;
102+ if ( key . leftArrow ) cycleLogFilter ( - 1 ) ;
103+ if ( key . rightArrow ) cycleLogFilter ( 1 ) ;
104+ return ;
38105 }
106+
39107 if ( key . tab ) {
40- setShowSelector ( true ) ;
108+ setOverlay ( 'selector' ) ;
109+ return ;
110+ }
111+ if ( key . upArrow ) {
112+ setSelectedProcess ( Math . max ( 0 , selectedIndex - 1 ) ) ;
113+ return ;
114+ }
115+ if ( key . downArrow ) {
116+ setSelectedProcess ( Math . min ( Math . max ( processes . length - 1 , 0 ) , selectedIndex + 1 ) ) ;
117+ return ;
118+ }
119+ if ( ( key . return || input === 'x' || input === 'X' ) && selectedInfo !== undefined ) {
120+ if ( monitor . snapshot ?. deployLock != null ) {
121+ monitor . appendEvent ( chalk . red ( 'Restart blocked: a deploy is in progress (lock held)' ) ) ;
122+ return ;
123+ }
124+ setOverlay ( 'confirmRestart' ) ;
125+ return ;
41126 }
42127 if ( input === 'l' || input === 'L' ) {
43128 setLiveMode ( ( v ) => {
@@ -51,37 +136,79 @@ export function App({ executor, config, app: initialApp, apps, targetName, host,
51136 return next ;
52137 } ) ;
53138 }
54- if ( input === 'r' || input === 'R' ) {
55- void monitor . refresh ( ) ;
56- monitor . appendEvent ( chalk . green ( 'Refresh triggered' ) ) ;
57- }
58139 } ) ;
59140
60- if ( showSelector ) {
141+ if ( overlay === 'help' ) {
142+ return < HelpOverlay /> ;
143+ }
144+
145+ if ( overlay === 'confirmRestart' && selectedInfo !== undefined ) {
146+ const dropsRequests = selectedInfo . execMode !== 'cluster' || selectedInfo . instances <= 1 ;
147+ return (
148+ < ConfirmDialog
149+ title = { `Restart ${ selectedInfo . pm2Name } ?` }
150+ lines = { dropsRequests ? [ 'Single-instance process — restart drops in-flight requests.' ] : [ ] }
151+ onConfirm = { ( ) => {
152+ void confirmRestart ( ) ;
153+ } }
154+ onCancel = { ( ) => setOverlay ( 'none' ) }
155+ />
156+ ) ;
157+ }
158+
159+ if ( overlay === 'selector' ) {
61160 return (
62161 < AppSelector
63162 apps = { apps }
64163 targetName = { targetName }
65164 onSelect = { ( app ) => {
66165 setCurrentApp ( app ) ;
67- setShowSelector ( false ) ;
166+ setOverlay ( 'none' ) ;
167+ setView ( 'dashboard' ) ;
168+ setLogFilter ( null ) ;
169+ setSelectedProcess ( 0 ) ;
68170 monitor . reset ( ) ;
69171 liveLogs . clearLogs ( ) ;
70172 setLiveMode ( false ) ;
71173 monitor . appendEvent ( chalk . green ( `Switched to ${ chalk . bold ( app . name ) } ` ) ) ;
72174 } }
73- onCancel = { ( ) => setShowSelector ( false ) }
175+ onCancel = { ( ) => setOverlay ( 'none' ) }
74176 />
75177 ) ;
76178 }
77179
180+ if ( view === 'logs' ) {
181+ const rows = stdout ?. rows ?? 24 ;
182+ const title =
183+ currentApp . appType === 'frontend'
184+ ? `Caddy Access Log — ${ currentApp . name } `
185+ : `Live Logs — ${ logFilter ?? 'all processes' } (←/→ filter, F back)` ;
186+ return (
187+ < MonitorFrame
188+ app = { currentApp }
189+ targetName = { targetName }
190+ host = { host }
191+ interval = { interval }
192+ liveMode = { liveActive }
193+ lastUpdate = { monitor . lastUpdate }
194+ snapshot = { monitor . snapshot }
195+ polling = { monitor . polling }
196+ error = { monitor . error }
197+ >
198+ < Box flexGrow = { 1 } >
199+ < LogPanel logBuffer = { liveLogs . logBuffer } maxLines = { Math . max ( 10 , rows - 6 ) } title = { title } />
200+ </ Box >
201+ </ MonitorFrame >
202+ ) ;
203+ }
204+
78205 return (
79206 < MonitorFrame
80207 app = { currentApp }
81208 targetName = { targetName }
82209 host = { host }
83210 interval = { interval }
84- liveMode = { liveMode }
211+ liveMode = { liveActive }
85212 lastUpdate = { monitor . lastUpdate }
86213 snapshot = { monitor . snapshot }
87214 polling = { monitor . polling }
@@ -90,35 +217,47 @@ export function App({ executor, config, app: initialApp, apps, targetName, host,
90217 < Box flexGrow = { 1 } flexDirection = "row" minHeight = { 8 } >
91218 < Box width = "60%" flexDirection = "column" >
92219 { currentApp . appType === 'frontend' ? (
93- < StaticFrontendPanel app = { currentApp } />
220+ < StaticFrontendPanel app = { currentApp } caddy = { monitor . snapshot ?. caddy ?? null } />
94221 ) : monitor . snapshot ? (
95222 < Pm2Panel
96223 processes = { monitor . snapshot . processes }
97224 cpuHistory = { monitor . history . cpu }
98225 memHistory = { monitor . history . memory }
226+ health = { monitor . snapshot . health }
227+ responseHistory = { monitor . history . responseMs }
228+ selectedIndex = { selectedIndex }
99229 />
100230 ) : (
101231 < WaitingPanel />
102232 ) }
103233 </ Box >
104234 < Box width = "40%" flexDirection = "column" >
105235 { monitor . snapshot ? (
106- < SystemPanel
107- system = { monitor . snapshot . system }
108- cpuHistory = { monitor . history . cpu }
109- memHistory = { monitor . history . memory }
110- />
236+ < >
237+ < SystemPanel
238+ system = { monitor . snapshot . system }
239+ cpuHistory = { monitor . history . cpu }
240+ memHistory = { monitor . history . memory }
241+ />
242+ { accessoryNames . length > 0 && (
243+ < AccessoriesPanel
244+ configuredNames = { accessoryNames }
245+ accessories = { monitor . snapshot . accessories }
246+ />
247+ ) }
248+ </ >
111249 ) : (
112250 < WaitingPanel />
113251 ) }
114252 </ Box >
115253 </ Box >
116254
117255 { monitor . snapshot && (
118- < Box height = { 7 } >
256+ < Box height = { accessoryNames . length > 0 ? 6 : 7 } >
119257 < ReleasePanel
120258 currentRelease = { monitor . snapshot . currentRelease }
121259 releases = { monitor . snapshot . releases }
260+ maxReleases = { accessoryNames . length > 0 ? 4 : 5 }
122261 />
123262 </ Box >
124263 ) }
0 commit comments