1+ import sharp from 'sharp' ;
12import { NINA_BASE_URL , UPDATE_THRESHOLD_COUNT } from './variables' ;
23
34interface NinaResponse < T > {
@@ -27,13 +28,16 @@ interface SequenceItem {
2728 Items ?: SequenceItem [ ] ;
2829 Name ?: string ;
2930 Triggers ?: SequenceItem [ ] ;
31+ TargetTime ?: string ;
32+ Delay ?: number ;
3033}
3134
3235export type LiveStatus = {
3336 active : boolean ;
3437 imageInfo ?: ImageHistoryItem ;
3538 mountInfo ?: MountInfo ;
3639 currentAction ?: string ;
40+ showImage ?: boolean ;
3741} ;
3842
3943function isNetworkError ( e : unknown , code : string ) : boolean {
@@ -77,7 +81,8 @@ export class NinaClient {
7781 }
7882 }
7983
80- private mapRunningAction ( name : string ) : string {
84+ private mapRunningAction ( item : SequenceItem ) : string {
85+ const name = item . Name || '' ;
8186 const map : Record < string , string > = {
8287 'Meridian Flip_Trigger' : 'Meridian flip' ,
8388 'Smart Exposure' : 'Exposing' ,
@@ -88,31 +93,44 @@ export class NinaClient {
8893 'AF After HFR Increase_Trigger' : 'Focusing' ,
8994 'Cool Camera' : 'Cooling Camera' ,
9095 'Wait for Time' : 'Waiting' ,
91- 'Wait for Time Span' : 'Waiting'
96+ 'Wait for Time Span' : 'Waiting' ,
97+ 'Warm Camera' : 'Warming Camera'
9298 } ;
9399
94- return map [ name ] || name ;
100+ let action = map [ name ] || name ;
101+
102+ if ( name === 'Wait for Time' && item . TargetTime ) {
103+ const target = new Date ( item . TargetTime ) . getTime ( ) ;
104+ const diff = Math . ceil ( ( target - Date . now ( ) ) / 1000 ) ;
105+ if ( diff > 0 ) {
106+ action += ` (${ diff } s)` ;
107+ }
108+ } else if ( name === 'Wait for Time Span' && item . Delay ) {
109+ action += ` for ${ item . Delay } s` ;
110+ }
111+
112+ return action ;
95113 }
96114
97115 // Helper to actually implement the recursive search properly aligned with logic
98- private getDeepestRunningName ( items : SequenceItem [ ] ) : string | undefined {
116+ private getDeepestRunningItem ( items : SequenceItem [ ] ) : SequenceItem | undefined {
99117 for ( const item of items ) {
100118 // Check if this item is RUNNING
101119 if ( item . Status === 'RUNNING' ) {
102120 // 1. Check Triggers for this item
103121 if ( item . Triggers && Array . isArray ( item . Triggers ) ) {
104122 const runningTrigger = item . Triggers . find ( ( t ) => t . Status === 'RUNNING' ) ;
105- if ( runningTrigger ) return runningTrigger . Name ;
123+ if ( runningTrigger ) return runningTrigger ;
106124 }
107125
108126 // 2. Check Children Items
109127 if ( item . Items && item . Items . length > 0 ) {
110- const deepName = this . getDeepestRunningName ( item . Items ) ;
111- if ( deepName ) return deepName ;
128+ const deepItem = this . getDeepestRunningItem ( item . Items ) ;
129+ if ( deepItem ) return deepItem ;
112130 }
113131
114132 // 3. If no children/triggers running, this is the deepest running item
115- return item . Name ;
133+ return item ;
116134 }
117135 }
118136 return undefined ;
@@ -136,11 +154,11 @@ export class NinaClient {
136154 const sequenceData =
137155 await this . fetch < NinaResponse < SequenceItem [ ] > > ( 'api/sequence/json' ) ;
138156
139- const deepestName =
157+ const deepestItem =
140158 sequenceData ?. Success && sequenceData . Response
141- ? this . getDeepestRunningName ( sequenceData . Response )
159+ ? this . getDeepestRunningItem ( sequenceData . Response )
142160 : undefined ;
143- const isRunning = ! ! deepestName ;
161+ const isRunning = ! ! deepestItem ;
144162
145163 if ( ! isRunning ) {
146164 this . cachedLiveStatus = { active : false } ;
@@ -177,11 +195,31 @@ export class NinaClient {
177195 this . lastUpdate = now ;
178196 }
179197
198+ const currentAction = this . mapRunningAction ( deepestItem ! ) ;
199+ const hiddenActions = [
200+ 'Meridian flip' ,
201+ 'Waiting' ,
202+ 'Cooling Camera' ,
203+ 'Warming Camera'
204+ ] ;
205+
206+ let showImage = ! hiddenActions . some ( ( hidden ) => currentAction . startsWith ( hidden ) ) ;
207+
208+ // Check if the last image is older than 10 minutes (600,000 ms)
209+ if ( imageInfo ?. Date && Date . now ( ) - new Date ( imageInfo . Date ) . getTime ( ) > 600000 ) {
210+ showImage = false ;
211+ }
212+
213+ if ( ! this . cachedLiveImage ) {
214+ showImage = false ;
215+ }
216+
180217 this . cachedLiveStatus = {
181218 active : true ,
182219 mountInfo : mountData ?. Response ,
183220 imageInfo,
184- currentAction : this . mapRunningAction ( deepestName ! )
221+ currentAction,
222+ showImage
185223 } ;
186224
187225 return this . cachedLiveStatus ;
@@ -191,7 +229,11 @@ export class NinaClient {
191229 try {
192230 const res = await fetch ( `${ this . baseUrl } /v2/api/prepared-image` ) ;
193231 if ( ! res . ok ) return ;
194- this . cachedLiveImage = Buffer . from ( await res . arrayBuffer ( ) ) ;
232+ const buffer = Buffer . from ( await res . arrayBuffer ( ) ) ;
233+
234+ this . cachedLiveImage = await sharp ( buffer )
235+ . jpeg ( { quality : 80 , mozjpeg : true } )
236+ . toBuffer ( ) ;
195237 } catch ( e ) {
196238 if ( isNetworkError ( e , 'ECONNREFUSED' ) || isNetworkError ( e , 'EHOSTUNREACH' ) ) {
197239 // Host unreachable, likely Nina is offline => don't spam errors
0 commit comments