@@ -398,6 +398,26 @@ function generateIconServiceUrls(domain: string): string[] {
398398 ] ;
399399}
400400
401+ /**
402+ * Generates dashboard-icons URLs for an app name.
403+ * Uses walkxcode/dashboard-icons as a final fallback for selfhosted apps.
404+ * Keeps matching conservative to avoid overriding valid site-specific icons.
405+ */
406+ function generateDashboardIconUrls ( appName : string ) : string [ ] {
407+ const baseUrl = 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png' ;
408+ const name = appName . toLowerCase ( ) . trim ( ) ;
409+ const slugs = new Set < string > ( ) ;
410+
411+ // Exact name
412+ slugs . add ( name ) ;
413+ // Replace spaces with hyphens
414+ slugs . add ( name . replace ( / \s + / g, '-' ) ) ;
415+
416+ return [ ...slugs ]
417+ . filter ( ( s ) => s . length > 0 )
418+ . map ( ( slug ) => `${ baseUrl } /${ slug } .png` ) ;
419+ }
420+
401421/**
402422 * Attempts to fetch favicon from website
403423 */
@@ -409,14 +429,14 @@ async function tryGetFavicon(
409429 const domain = new URL ( url ) . hostname ;
410430 const spinner = getSpinner ( `Fetching icon from ${ domain } ...` ) ;
411431
412- const serviceUrls = generateIconServiceUrls ( domain ) ;
413-
414432 const isCI =
415433 process . env . CI === 'true' || process . env . GITHUB_ACTIONS === 'true' ;
416434 const downloadTimeout = isCI
417435 ? ICON_CONFIG . downloadTimeout . ci
418436 : ICON_CONFIG . downloadTimeout . default ;
419437
438+ const serviceUrls = generateIconServiceUrls ( domain ) ;
439+
420440 for ( const serviceUrl of serviceUrls ) {
421441 try {
422442 const faviconPath = await downloadIcon (
@@ -445,6 +465,37 @@ async function tryGetFavicon(
445465 }
446466 }
447467
468+ // Final fallback for selfhosted apps behind auth where domain-based
469+ // services cannot access the site favicon.
470+ if ( appName ) {
471+ const dashboardIconUrls = generateDashboardIconUrls ( appName ) ;
472+ for ( const iconUrl of dashboardIconUrls ) {
473+ try {
474+ const iconPath = await downloadIcon ( iconUrl , false , downloadTimeout ) ;
475+ if ( ! iconPath ) continue ;
476+
477+ const convertedPath = await convertIconFormat ( iconPath , appName ) ;
478+ if ( convertedPath ) {
479+ const finalPath = await copyWindowsIconIfNeeded (
480+ convertedPath ,
481+ appName ,
482+ ) ;
483+ spinner . succeed (
484+ chalk . green (
485+ `Icon found via dashboard-icons fallback for "${ appName } "!` ,
486+ ) ,
487+ ) ;
488+ return finalPath ;
489+ }
490+ } catch ( error : unknown ) {
491+ if ( error instanceof Error ) {
492+ logger . debug ( `Dashboard icon ${ iconUrl } failed: ${ error . message } ` ) ;
493+ }
494+ continue ;
495+ }
496+ }
497+ }
498+
448499 spinner . warn ( `No favicon found for ${ domain } . Using default.` ) ;
449500 return null ;
450501 } catch ( error ) {
0 commit comments