@@ -388,17 +388,12 @@ export class ExternalServicesManager {
388388 settingsContent . appendChild ( saveButton ) ;
389389 }
390390
391+ // ============ Card Creation Helpers ============
392+
391393 /**
392- * Display static service card (no API/feed )
394+ * Create service card header (icon + info )
393395 */
394- displayStaticServiceCard ( container , serviceKey , serviceDef ) {
395- const serviceLink = document . createElement ( "a" ) ;
396- serviceLink . href = serviceDef . url ;
397- serviceLink . target = "_blank" ;
398- serviceLink . rel = "noopener noreferrer" ;
399- serviceLink . className = "external-service-card static" ;
400- serviceLink . dataset . serviceKey = serviceKey ;
401-
396+ createServiceCardHeader ( serviceDef , statusClassName , statusContent ) {
402397 const headerDiv = document . createElement ( "div" ) ;
403398 headerDiv . className = "service-header" ;
404399
@@ -413,79 +408,101 @@ export class ExternalServicesManager {
413408 h3 . textContent = serviceDef . name ;
414409
415410 const statusSpan = document . createElement ( "span" ) ;
416- statusSpan . className = "service-status status-info" ;
417- statusSpan . innerHTML = `<i class="fas fa-external-link-alt"></i> ` ;
418- statusSpan . appendChild ( document . createTextNode ( serviceDef . statusText || 'Visit status page' ) ) ;
411+ statusSpan . className = `service-status ${ statusClassName } ` ;
412+ statusSpan . innerHTML = statusContent ;
419413
420414 infoDiv . appendChild ( h3 ) ;
421415 infoDiv . appendChild ( statusSpan ) ;
422416 headerDiv . appendChild ( iconDiv ) ;
423417 headerDiv . appendChild ( infoDiv ) ;
424- serviceLink . appendChild ( headerDiv ) ;
425418
426- container . appendChild ( serviceLink ) ;
419+ return headerDiv ;
427420 }
428421
429422 /**
430- * Display service card with loading state
423+ * Create base service card element
431424 */
432- displayServiceCardWithLoadingState ( container , serviceKey , serviceDef ) {
425+ createBaseServiceCard ( serviceKey , serviceDef , cardClass , headerElement ) {
433426 const serviceLink = document . createElement ( "a" ) ;
434427 serviceLink . href = serviceDef . url ;
435428 serviceLink . target = "_blank" ;
436429 serviceLink . rel = "noopener noreferrer" ;
437- serviceLink . className = " external-service-card loading" ;
430+ serviceLink . className = ` external-service-card ${ cardClass } ` ;
438431 serviceLink . dataset . serviceKey = serviceKey ;
432+ serviceLink . appendChild ( headerElement ) ;
433+ return serviceLink ;
434+ }
435+
436+ /**
437+ * Display static service card (no API/feed)
438+ */
439+ displayStaticServiceCard ( container , serviceKey , serviceDef ) {
440+ const statusContent = `<i class="fas fa-external-link-alt"></i> ` ;
441+ const contentNode = document . createTextNode ( serviceDef . statusText || 'Visit status page' ) ;
442+ const headerDiv = this . createServiceCardHeader ( serviceDef , "status-info" , statusContent ) ;
443+ const statusSpan = headerDiv . querySelector ( ".service-status" ) ;
444+ statusSpan . appendChild ( contentNode ) ;
439445
440- const headerDiv = document . createElement ( "div" ) ;
441- headerDiv . className = "service-header" ;
442-
443- const iconDiv = document . createElement ( "div" ) ;
444- iconDiv . className = `service-icon ${ serviceDef . color } ` ;
445- iconDiv . innerHTML = `<i class="fas ${ serviceDef . icon } "></i>` ;
446-
447- const infoDiv = document . createElement ( "div" ) ;
448- infoDiv . className = "service-info" ;
449-
450- const h3 = document . createElement ( "h3" ) ;
451- h3 . textContent = serviceDef . name ;
452-
453- const statusSpan = document . createElement ( "span" ) ;
454- statusSpan . className = "service-status status-loading" ;
455- statusSpan . innerHTML = `<i class="fas fa-spinner fa-spin"></i> ` ;
456- statusSpan . appendChild ( document . createTextNode ( "Loading..." ) ) ;
457-
458- infoDiv . appendChild ( h3 ) ;
459- infoDiv . appendChild ( statusSpan ) ;
460- headerDiv . appendChild ( iconDiv ) ;
461- headerDiv . appendChild ( infoDiv ) ;
462- serviceLink . appendChild ( headerDiv ) ;
446+ const serviceLink = this . createBaseServiceCard ( serviceKey , serviceDef , "static" , headerDiv ) ;
447+ container . appendChild ( serviceLink ) ;
448+ }
449+
450+ /**
451+ * Display service card with loading state
452+ */
453+ displayServiceCardWithLoadingState ( container , serviceKey , serviceDef ) {
454+ const statusContent = `<i class="fas fa-spinner fa-spin"></i> ` ;
455+ const contentNode = document . createTextNode ( "Loading..." ) ;
456+ const headerDiv = this . createServiceCardHeader ( serviceDef , "status-loading" , statusContent ) ;
457+ const statusSpan = headerDiv . querySelector ( ".service-status" ) ;
458+ statusSpan . appendChild ( contentNode ) ;
463459
460+ const serviceLink = this . createBaseServiceCard ( serviceKey , serviceDef , "loading" , headerDiv ) ;
464461 container . appendChild ( serviceLink ) ;
465462 }
466463
464+ // ============ Status Update Helpers ============
465+
467466 /**
468- * Update feed-based service status asynchronously
467+ * Extract and determine status display values
469468 */
470- async updateFeedServiceStatus ( serviceKey , serviceDef ) {
471- try {
472- // Check cache first
473- let data = this . getCachedService ( serviceKey ) ;
469+ getStatusDisplayValues ( statusIndicator ) {
470+ const statusClass = statusIndicator === "none" ? "operational" : statusIndicator ;
471+ const statusIcon = statusClass === "operational" ? "check-circle" : "exclamation-triangle" ;
472+ const statusColor = statusClass === "operational" ? "success" : statusClass === "minor" ? "warning" : "error" ;
473+ return { statusClass, statusIcon, statusColor } ;
474+ }
475+
476+ /**
477+ * Update service card with status data
478+ */
479+ updateServiceCardStatus ( serviceCard , statusDescription , statusClass , statusIcon , statusColor ) {
480+ // Update card class
481+ serviceCard . classList . remove ( "loading" , "error" ) ;
482+
483+ // Update status span
484+ const statusSpan = serviceCard . querySelector ( ".service-status" ) ;
485+ if ( statusSpan ) {
486+ statusSpan . className = `service-status status-${ statusColor } ` ;
487+ statusSpan . innerHTML = `<i class="fas fa-${ statusIcon } "></i> ` ;
488+ statusSpan . appendChild ( document . createTextNode ( this . utils . sanitizeInput ( statusDescription ) ) ) ;
489+ }
490+ }
491+
492+ /**
493+ * Fetch data with timeout and caching support
494+ */
495+ async fetchServiceData ( fetchFn , serviceKey ) {
496+ // Check cache first
497+ let data = this . getCachedService ( serviceKey ) ;
498+
499+ if ( ! data ) {
500+ // Not in cache, fetch from API
501+ const controller = new AbortController ( ) ;
502+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , 60000 ) ;
474503
475- if ( ! data ) {
476- // Not in cache, fetch from API
477- const controller = new AbortController ( ) ;
478- const timeoutId = setTimeout ( ( ) => controller . abort ( ) , 60000 ) ;
479-
480- let apiUrl = `/api/external-services/feed?feed=${ encodeURIComponent ( serviceDef . feedType ) } ` ;
481- if ( serviceDef . feedFilter ) {
482- apiUrl += `&filter=${ encodeURIComponent ( serviceDef . feedFilter ) } ` ;
483- }
484-
485- const response = await fetch ( apiUrl , {
486- signal : controller . signal ,
487- credentials : 'include'
488- } ) ;
504+ try {
505+ const response = await fetchFn ( controller . signal ) ;
489506 clearTimeout ( timeoutId ) ;
490507
491508 if ( ! response . ok ) {
@@ -496,7 +513,31 @@ export class ExternalServicesManager {
496513
497514 // Cache the response
498515 this . setCachedService ( serviceKey , data ) ;
516+ } catch ( error ) {
517+ clearTimeout ( timeoutId ) ;
518+ throw error ;
499519 }
520+ }
521+
522+ return data ;
523+ }
524+
525+ /**
526+ * Update feed-based service status asynchronously
527+ */
528+ async updateFeedServiceStatus ( serviceKey , serviceDef ) {
529+ try {
530+ const data = await this . fetchServiceData ( ( signal ) => {
531+ let apiUrl = `/api/external-services/feed?feed=${ encodeURIComponent ( serviceDef . feedType ) } ` ;
532+ if ( serviceDef . feedFilter ) {
533+ apiUrl += `&filter=${ encodeURIComponent ( serviceDef . feedFilter ) } ` ;
534+ }
535+
536+ return fetch ( apiUrl , {
537+ signal : signal ,
538+ credentials : 'include'
539+ } ) ;
540+ } , serviceKey ) ;
500541
501542 if ( ! data || ! data . status ) {
502543 throw new Error ( 'Invalid feed response format' ) ;
@@ -509,20 +550,8 @@ export class ExternalServicesManager {
509550 return ;
510551 }
511552
512- const statusClass = data . status . indicator === "none" ? "operational" : data . status . indicator ;
513- const statusIcon = statusClass === "operational" ? "check-circle" : "exclamation-triangle" ;
514- const statusColor = statusClass === "operational" ? "success" : statusClass === "minor" ? "warning" : "error" ;
515-
516- // Update card class
517- serviceCard . classList . remove ( "loading" , "error" ) ;
518-
519- // Update status span
520- const statusSpan = serviceCard . querySelector ( ".service-status" ) ;
521- if ( statusSpan ) {
522- statusSpan . className = `service-status status-${ statusColor } ` ;
523- statusSpan . innerHTML = `<i class="fas fa-${ statusIcon } "></i> ` ;
524- statusSpan . appendChild ( document . createTextNode ( this . utils . sanitizeInput ( data . status . description ) ) ) ;
525- }
553+ const { statusClass, statusIcon, statusColor } = this . getStatusDisplayValues ( data . status . indicator ) ;
554+ this . updateServiceCardStatus ( serviceCard , data . status . description , statusClass , statusIcon , statusColor ) ;
526555 } catch ( error ) {
527556 console . error ( `Failed to load ${ serviceDef . name } feed status:` , error ) ;
528557 this . handleServiceError ( serviceKey , serviceDef , error ) ;
@@ -534,28 +563,11 @@ export class ExternalServicesManager {
534563 */
535564 async updateStatusPageServiceStatus ( serviceKey , serviceDef ) {
536565 try {
537- // Check cache first
538- let data = this . getCachedService ( serviceKey ) ;
539-
540- if ( ! data ) {
541- // Not in cache, fetch from API
542- const controller = new AbortController ( ) ;
543- const timeoutId = setTimeout ( ( ) => controller . abort ( ) , 60000 ) ;
544-
545- const response = await fetch ( serviceDef . api , {
546- signal : controller . signal
566+ const data = await this . fetchServiceData ( ( signal ) => {
567+ return fetch ( serviceDef . api , {
568+ signal : signal
547569 } ) ;
548- clearTimeout ( timeoutId ) ;
549-
550- if ( ! response . ok ) {
551- throw new Error ( `HTTP error! status: ${ response . status } ` ) ;
552- }
553-
554- data = await response . json ( ) ;
555-
556- // Cache the response
557- this . setCachedService ( serviceKey , data ) ;
558- }
570+ } , serviceKey ) ;
559571
560572 if ( ! data || ! data . status || ! data . status . indicator ) {
561573 throw new Error ( 'Invalid API response format' ) ;
@@ -568,20 +580,8 @@ export class ExternalServicesManager {
568580 return ;
569581 }
570582
571- const statusClass = data . status . indicator === "none" ? "operational" : data . status . indicator ;
572- const statusIcon = statusClass === "operational" ? "check-circle" : "exclamation-triangle" ;
573- const statusColor = statusClass === "operational" ? "success" : statusClass === "minor" ? "warning" : "error" ;
574-
575- // Update card class
576- serviceCard . classList . remove ( "loading" , "error" ) ;
577-
578- // Update status span
579- const statusSpan = serviceCard . querySelector ( ".service-status" ) ;
580- if ( statusSpan ) {
581- statusSpan . className = `service-status status-${ statusColor } ` ;
582- statusSpan . innerHTML = `<i class="fas fa-${ statusIcon } "></i> ` ;
583- statusSpan . appendChild ( document . createTextNode ( this . utils . sanitizeInput ( data . status . description ) ) ) ;
584- }
583+ const { statusClass, statusIcon, statusColor } = this . getStatusDisplayValues ( data . status . indicator ) ;
584+ this . updateServiceCardStatus ( serviceCard , data . status . description , statusClass , statusIcon , statusColor ) ;
585585 } catch ( error ) {
586586 console . error ( `Failed to load ${ serviceDef . name } status:` , error ) ;
587587 this . handleServiceError ( serviceKey , serviceDef , error ) ;
@@ -773,14 +773,12 @@ export class ExternalServicesManager {
773773 enableServiceDragDrop ( container ) {
774774 const serviceCards = container . querySelectorAll ( '.external-service-card' ) ;
775775 let draggedElement = null ;
776- let draggedServiceKey = null ;
777776
778777 serviceCards . forEach ( card => {
779778 card . draggable = true ;
780779
781780 card . addEventListener ( 'dragstart' , ( e ) => {
782781 draggedElement = card ;
783- draggedServiceKey = card . dataset . serviceKey ;
784782 card . classList . add ( 'dragging' ) ;
785783 e . dataTransfer . effectAllowed = 'move' ;
786784 e . dataTransfer . setData ( 'text/html' , card . innerHTML ) ;
@@ -818,7 +816,6 @@ export class ExternalServicesManager {
818816 targetCard . classList . remove ( 'drag-over' ) ;
819817
820818 // Swap positions
821- const targetServiceKey = targetCard . dataset . serviceKey ;
822819 const allCards = Array . from ( container . querySelectorAll ( '.external-service-card' ) ) ;
823820 const draggedIndex = allCards . indexOf ( draggedElement ) ;
824821 const targetIndex = allCards . indexOf ( targetCard ) ;
0 commit comments