@@ -12,6 +12,7 @@ export const STELLAR_RECONNECT_MAX_MS = 30000
1212export const STELLAR_TOKEN_POLL_INTERVAL_MS = 100
1313export const STELLAR_TOKEN_POLL_MAX_ATTEMPTS = 30
1414export const STELLAR_MISSION_TRIGGER_EVENT = 'stellar:mission_trigger'
15+ const STELLAR_REFRESH_REQUEST_COUNT = 7
1516
1617export interface StellarMissionTriggerPayload {
1718 solveId : string
@@ -53,6 +54,12 @@ function getStoredStellarBatchIntervalMs(): number {
5354 return resolveStellarBatchIntervalMs ( safeGetItem ( STORAGE_KEY_STELLAR_BATCH_INTERVAL_MS ) )
5455}
5556
57+ function getErrorMessage ( error : unknown , fallback : string ) : string {
58+ if ( error instanceof Error && error . message . trim ( ) ) return error . message
59+ if ( typeof error === 'string' && error . trim ( ) ) return error
60+ return fallback
61+ }
62+
5663export function useStellarSource ( ) {
5764 const [ isConnected , setIsConnected ] = useState ( false )
5865 const [ connectionError , setConnectionError ] = useState < string | null > ( null )
@@ -92,6 +99,9 @@ export function useStellarSource() {
9299 const [ nextBatchAtMs , setNextBatchAtMs ] = useState ( ( ) => getNextBatchTime ( batchIntervalMs ) )
93100 const [ isBatchRefreshing , setIsBatchRefreshing ] = useState ( false )
94101 const batchRefreshInFlightRef = useRef ( false )
102+ const setOperationalError = useCallback ( ( error : unknown , fallback : string ) => {
103+ setConnectionError ( getErrorMessage ( error , fallback ) )
104+ } , [ ] )
95105
96106 useEffect ( ( ) => {
97107 const handleStorage = ( event : StorageEvent ) => {
@@ -131,8 +141,19 @@ export function useStellarSource() {
131141 if ( results [ 5 ] . status === 'fulfilled' ) setSolves ( results [ 5 ] . value || [ ] )
132142 if ( results [ 6 ] . status === 'fulfilled' ) setActivity ( results [ 6 ] . value || [ ] )
133143 const failures = results . filter ( result => result . status === 'rejected' )
134- if ( failures . length > 0 ) console . warn ( 'stellar: refreshState partial failure —' , failures . length , 'of 7 calls failed' )
135- } , [ setNotifications ] )
144+ if ( failures . length === 0 ) {
145+ setConnectionError ( null )
146+ return
147+ }
148+ console . warn ( 'stellar: refreshState partial failure —' , failures . length , 'of' , STELLAR_REFRESH_REQUEST_COUNT , 'calls failed' )
149+ const firstFailure = failures [ 0 ]
150+ setOperationalError (
151+ firstFailure . status === 'rejected' ? firstFailure . reason : null ,
152+ failures . length === 1
153+ ? 'Failed to refresh Stellar state'
154+ : `Failed to refresh Stellar state (${ failures . length } /${ STELLAR_REFRESH_REQUEST_COUNT } requests failed)` ,
155+ )
156+ } , [ setNotifications , setOperationalError ] )
136157 const scheduleNextBatch = useCallback ( ( intervalMs = batchIntervalMsRef . current ) => {
137158 setNextBatchAtMs ( getNextBatchTime ( intervalMs ) )
138159 } , [ ] )
@@ -144,12 +165,13 @@ export function useStellarSource() {
144165 await refreshState ( )
145166 } catch ( err ) {
146167 console . warn ( 'stellar: batch refresh failed:' , err )
168+ setOperationalError ( err , 'Failed to refresh Stellar state' )
147169 } finally {
148170 batchRefreshInFlightRef . current = false
149171 setIsBatchRefreshing ( false )
150172 scheduleNextBatch ( )
151173 }
152- } , [ refreshState , scheduleNextBatch ] )
174+ } , [ refreshState , scheduleNextBatch , setOperationalError ] )
153175 const setBatchIntervalMs = useCallback ( ( intervalMs : number ) => {
154176 const nextIntervalMs = resolveStellarBatchIntervalMs ( intervalMs )
155177 batchIntervalMsRef . current = nextIntervalMs
@@ -193,6 +215,7 @@ export function useStellarSource() {
193215 } )
194216 stellarApi . startSolve ( notif . id ) . catch ( err => {
195217 console . warn ( 'stellar: auto-solve for critical event failed:' , notif . id , err )
218+ setOperationalError ( err , 'Failed to auto-solve critical event' )
196219 setSolveProgress ( prev => {
197220 const copy = { ...prev }
198221 delete copy [ notif . id ]
@@ -267,7 +290,7 @@ export function useStellarSource() {
267290 on < StellarMissionTriggerPayload > ( 'mission_trigger' , payload => {
268291 window . dispatchEvent ( new CustomEvent ( STELLAR_MISSION_TRIGGER_EVENT , { detail : payload } ) )
269292 } )
270- } , [ setNotifications ] )
293+ } , [ setNotifications , setOperationalError ] )
271294
272295 useEffect ( ( ) => {
273296 reconnectRef . current = connectSSE
@@ -308,6 +331,7 @@ export function useStellarSource() {
308331 await refreshState ( )
309332 } catch ( err ) {
310333 console . warn ( 'stellar: init failed:' , err )
334+ setOperationalError ( err , 'Failed to initialize Stellar state' )
311335 }
312336 scheduleNextBatch ( )
313337 connectSSE ( )
@@ -328,7 +352,7 @@ export function useStellarSource() {
328352 }
329353 esRef . current ?. close ( )
330354 }
331- } , [ connectSSE , refreshState , scheduleNextBatch ] )
355+ } , [ connectSSE , refreshState , scheduleNextBatch , setOperationalError ] )
332356
333357 const unreadCount = notifications . filter ( item => ! item . read ) . length
334358 const acknowledgeNotification = useCallback ( async ( id : string ) => {
0 commit comments