1- import { Box , Text , useInput , useApp } from 'ink' ;
2- import { useEffect , useState , useRef } from 'react' ;
1+ import { Box , useInput , useApp } from 'ink' ;
2+ import { useState } from 'react' ;
33import type { RemoteExecutor } from '../../domain/remote/executor.js' ;
44import type { ShipnodeConfig , ShipnodeApp } from '../../shared/types.js' ;
5- import { MetricsHistory , type MetricsSnapshot } from './state.js' ;
6- import { collectMetrics , collectLogs } from './poller.js' ;
75import { Pm2Panel } from './panels/Pm2Panel.js' ;
86import { SystemPanel } from './panels/SystemPanel.js' ;
97import { ReleasePanel } from './panels/ReleasePanel.js' ;
108import { LogPanel } from './panels/LogPanel.js' ;
9+ import { EventsPanel } from './panels/EventsPanel.js' ;
10+ import { StaticFrontendPanel } from './panels/StaticFrontendPanel.js' ;
1111import { AppSelector } from './app-selector.js' ;
1212import chalk from 'chalk' ;
13-
14- const ACCENT = '#d6a85d' ;
15- const BG = '#0d1117' ;
16- const BG_HEADER = '#161b22' ;
17- const BORDER = '#30363d' ;
13+ import { useMonitorData } from './hooks/use-monitor-data.js' ;
14+ import { useLiveLogs } from './hooks/use-live-logs.js' ;
15+ import { MonitorFrame , WaitingPanel } from './layout/MonitorFrame.js' ;
1816
1917interface AppProps {
2018 executor : RemoteExecutor ;
2119 config : ShipnodeConfig ;
2220 app : ShipnodeApp ;
21+ apps : ShipnodeApp [ ] ;
22+ targetName : string ;
23+ host : string ;
2324 interval : number ;
2425}
2526
26- export function App ( { executor, config, app : initialApp , interval } : AppProps ) {
27+ export function App ( { executor, config, app : initialApp , apps , targetName , host , interval } : AppProps ) {
2728 const { exit } = useApp ( ) ;
2829 const [ currentApp , setCurrentApp ] = useState ( initialApp ) ;
29- const [ snapshot , setSnapshot ] = useState < MetricsSnapshot | null > ( null ) ;
30- const historyRef = useRef ( new MetricsHistory ( ) ) ;
3130 const [ liveMode , setLiveMode ] = useState ( false ) ;
32- const [ logBuffer , setLogBuffer ] = useState ( '' ) ;
33- const [ events , setEvents ] = useState < string [ ] > ( [ ] ) ;
3431 const [ showSelector , setShowSelector ] = useState ( false ) ;
35- const [ error , setError ] = useState < string | null > ( null ) ;
36- const [ lastUpdate , setLastUpdate ] = useState < string > ( '' ) ;
37-
38- const history = historyRef . current ;
39-
40- const appendEvent = ( msg : string ) => {
41- setEvents ( ( prev ) => [ ...prev . slice ( - 100 ) , msg ] ) ;
42- } ;
43-
44- const doPoll = async ( ) => {
45- try {
46- const snap = await collectMetrics ( executor , currentApp , config ) ;
47- setSnapshot ( snap ) ;
48- history . push ( snap ) ;
49- setLastUpdate ( new Date ( ) . toLocaleTimeString ( ) ) ;
50- setError ( null ) ;
51- } catch ( err ) {
52- const msg = err instanceof Error ? err . message : String ( err ) ;
53- setError ( msg ) ;
54- appendEvent ( chalk . red ( `Poll failed: ${ msg } ` ) ) ;
55- }
56- } ;
57-
58- const doLogPoll = async ( ) => {
59- try {
60- const namespace = currentApp . pm2 ?. apps [ 0 ] ?. name ?? '' ;
61- if ( ! namespace ) return ;
62- const freshLogs = await collectLogs ( executor , namespace , 20 ) ;
63- if ( freshLogs && freshLogs !== '(no logs)' ) {
64- setLogBuffer ( ( prev ) => {
65- const combined = prev
66- ? prev . split ( '\n' ) . slice ( - 50 ) . join ( '\n' ) + '\n' + freshLogs
67- : freshLogs ;
68- return combined ;
69- } ) ;
70- }
71- } catch {
72- // best-effort
73- }
74- } ;
75-
76- useEffect ( ( ) => {
77- doPoll ( ) ;
78- const timer = setInterval ( doPoll , interval * 1000 ) ;
79- return ( ) => clearInterval ( timer ) ;
80- } , [ currentApp , interval ] ) ;
81-
82- useEffect ( ( ) => {
83- if ( ! liveMode ) return ;
84- const timer = setInterval ( doLogPoll , interval * 1000 ) ;
85- return ( ) => clearInterval ( timer ) ;
86- } , [ liveMode , currentApp , interval ] ) ;
32+ const monitor = useMonitorData ( executor , config , currentApp , interval ) ;
33+ const liveLogs = useLiveLogs ( executor , currentApp , liveMode , interval ) ;
8734
8835 useInput ( ( input , key ) => {
8936 if ( input === 'q' ) {
@@ -96,143 +43,93 @@ export function App({ executor, config, app: initialApp, interval }: AppProps) {
9643 setLiveMode ( ( v ) => {
9744 const next = ! v ;
9845 if ( next ) {
99- appendEvent ( chalk . yellow ( 'Live logs enabled' ) ) ;
46+ monitor . appendEvent ( chalk . yellow ( 'Live logs enabled' ) ) ;
10047 } else {
101- setLogBuffer ( '' ) ;
102- appendEvent ( chalk . yellow ( 'Live logs disabled' ) ) ;
48+ liveLogs . clearLogs ( ) ;
49+ monitor . appendEvent ( chalk . yellow ( 'Live logs disabled' ) ) ;
10350 }
10451 return next ;
10552 } ) ;
10653 }
10754 if ( input === 'r' || input === 'R' ) {
108- doPoll ( ) ;
109- appendEvent ( chalk . green ( 'Refresh triggered' ) ) ;
55+ void monitor . refresh ( ) ;
56+ monitor . appendEvent ( chalk . green ( 'Refresh triggered' ) ) ;
11057 }
11158 } ) ;
11259
11360 if ( showSelector ) {
11461 return (
11562 < AppSelector
116- apps = { config . apps }
63+ apps = { apps }
64+ targetName = { targetName }
11765 onSelect = { ( app ) => {
11866 setCurrentApp ( app ) ;
11967 setShowSelector ( false ) ;
120- history . clear ( ) ;
121- setLogBuffer ( '' ) ;
68+ monitor . reset ( ) ;
69+ liveLogs . clearLogs ( ) ;
12270 setLiveMode ( false ) ;
123- setSnapshot ( null ) ;
124- appendEvent ( chalk . green ( `Switched to ${ chalk . bold ( app . name ) } ` ) ) ;
71+ monitor . appendEvent ( chalk . green ( `Switched to ${ chalk . bold ( app . name ) } ` ) ) ;
12572 } }
12673 onCancel = { ( ) => setShowSelector ( false ) }
12774 />
12875 ) ;
12976 }
13077
131- const host = currentApp . domain ?? 'no domain' ;
132-
13378 return (
134- < Box flexDirection = "column" height = "100%" backgroundColor = { BG } >
135- { /* Header */ }
136- < Box height = { 1 } backgroundColor = { BG_HEADER } paddingLeft = { 1 } borderBottom = { false } >
137- < Text bold color = { ACCENT } >
138- ShipNode Monitor{ ' ' }
139- </ Text >
140- < Text >
141- — { currentApp . name } ({ currentApp . appType } ) —{ ' ' }
142- </ Text >
143- < Text dimColor > { host } </ Text >
144- < Text dimColor >
145- { ' │' } interval: { interval } s
146- </ Text >
147- { liveMode ? (
148- < Text >
149- { ' │' } logs: < Text color = "green" > ON</ Text >
150- </ Text >
151- ) : (
152- < Text dimColor > │ logs: off</ Text >
153- ) }
154- < Text dimColor >
155- { ' │' } [Q]uit [Tab]switch app [L]toggle logs [R]refresh
156- </ Text >
157- </ Box >
158-
159- { /* Main content: PM2 + System */ }
79+ < MonitorFrame
80+ app = { currentApp }
81+ targetName = { targetName }
82+ host = { host }
83+ interval = { interval }
84+ liveMode = { liveMode }
85+ lastUpdate = { monitor . lastUpdate }
86+ snapshot = { monitor . snapshot }
87+ polling = { monitor . polling }
88+ error = { monitor . error }
89+ >
16090 < Box flexGrow = { 1 } flexDirection = "row" minHeight = { 8 } >
16191 < Box width = "60%" flexDirection = "column" >
162- { snapshot ? (
92+ { currentApp . appType === 'frontend' ? (
93+ < StaticFrontendPanel app = { currentApp } />
94+ ) : monitor . snapshot ? (
16395 < Pm2Panel
164- processes = { snapshot . processes }
165- cpuHistory = { history . cpu }
166- memHistory = { history . memory }
96+ processes = { monitor . snapshot . processes }
97+ cpuHistory = { monitor . history . cpu }
98+ memHistory = { monitor . history . memory }
16799 />
168100 ) : (
169- < Box borderStyle = "round" borderColor = { BORDER } padding = { 1 } flexGrow = { 1 } backgroundColor = { BG } >
170- < Text dimColor > Waiting for data...</ Text >
171- </ Box >
101+ < WaitingPanel />
172102 ) }
173103 </ Box >
174104 < Box width = "40%" flexDirection = "column" >
175- { snapshot ? (
105+ { monitor . snapshot ? (
176106 < SystemPanel
177- system = { snapshot . system }
178- cpuHistory = { history . cpu }
179- memHistory = { history . memory }
107+ system = { monitor . snapshot . system }
108+ cpuHistory = { monitor . history . cpu }
109+ memHistory = { monitor . history . memory }
180110 />
181111 ) : (
182- < Box borderStyle = "round" borderColor = { BORDER } padding = { 1 } flexGrow = { 1 } backgroundColor = { BG } >
183- < Text dimColor > Waiting for data...</ Text >
184- </ Box >
112+ < WaitingPanel />
185113 ) }
186114 </ Box >
187115 </ Box >
188116
189- { /* Release panel */ }
190- { snapshot && (
117+ { monitor . snapshot && (
191118 < Box height = { 7 } >
192119 < ReleasePanel
193- currentRelease = { snapshot . currentRelease }
194- releases = { snapshot . releases }
120+ currentRelease = { monitor . snapshot . currentRelease }
121+ releases = { monitor . snapshot . releases }
195122 />
196123 </ Box >
197124 ) }
198125
199- { /* Bottom panel: events or logs */ }
200126 < Box height = { 8 } >
201127 { liveMode ? (
202- < LogPanel logBuffer = { logBuffer } />
128+ < LogPanel logBuffer = { liveLogs . logBuffer } />
203129 ) : (
204- < Box
205- borderStyle = "round"
206- borderColor = { BORDER }
207- padding = { 1 }
208- flexDirection = "column"
209- flexGrow = { 1 }
210- backgroundColor = { BG }
211- >
212- < Text bold color = { ACCENT } >
213- Events
214- </ Text >
215- { events . length === 0 ? (
216- < Text dimColor > No events yet. Press [L] to enable live logs.</ Text >
217- ) : (
218- events . map ( ( e , i ) => < Text key = { i } > { e } </ Text > )
219- ) }
220- </ Box >
221- ) }
222- </ Box >
223-
224- { /* Status bar */ }
225- < Box height = { 1 } backgroundColor = { BG_HEADER } >
226- < Text dimColor >
227- { ' ' } Last update: { lastUpdate || '—' } { ' │ ' }
228- { snapshot ? `${ snapshot . processes . length } process(es)` : '—' }
229- </ Text >
230- { error && (
231- < Text color = "red" dimColor >
232- { ' │ ' } Error: { error }
233- </ Text >
130+ < EventsPanel events = { monitor . events } />
234131 ) }
235132 </ Box >
236- </ Box >
133+ </ MonitorFrame >
237134 ) ;
238135}
0 commit comments