@@ -687,6 +687,42 @@ const normalizeSetupAssistantMatchText = (value) => (
687687 . trim ( )
688688) ;
689689
690+ const SETUP_ASSISTANT_TEMPLATE_FALLBACK_BY_TYPE = Object . freeze ( {
691+ docker : 'Utilities' ,
692+ vm : 'Utility VMs'
693+ } ) ;
694+
695+ const getSetupAssistantBlueprintHeuristicMap = ( type , blueprintName ) => {
696+ const resolvedType = normalizeManagedType ( type ) ;
697+ const normalizedName = normalizeSetupAssistantMatchText ( blueprintName ) . replace ( / \s + / g, '-' ) ;
698+ if ( resolvedType === 'docker' ) {
699+ const dockerMap = {
700+ media : { contains : [ 'seerr' , 'wizarr' , 'listenarr' , 'cleanuparr' , 'agregarr' , 'watch' , 'request' , 'discover' ] , pathContains : [ 'media' , 'movies' , 'shows' , 'tv' , 'music' , 'books' , 'audiobooks' , 'anime' , 'comics' , 'photos' ] } ,
701+ downloads : { contains : [ 'download' , 'torrent' , 'nzb' , 'slsk' , 'seed' ] } ,
702+ monitoring : { contains : [ 'myspeed' , 'speedtest' , 'latency' , 'uptime' , 'metrics' , 'telemetry' ] } ,
703+ 'cloud-&-sync' : { contains : [ 'nextcloud' , 'owncloud' , 'seafile' , 'cloud' , 'sync' , 'drive' , 'collabora' , 'onlyoffice' ] } ,
704+ notifications : { contains : [ 'notify' , 'notification' , 'ntfy' , 'gotify' , 'apprise' , 'notifiarr' , 'pushover' , 'webhook' ] } ,
705+ utilities : { contains : [ 'qdirstat' , 'diskspeed' , 'ncdu' , 'baobab' , 'icons' , 'icon' , 'tool' , 'utility' , 'manager' ] , pathContains : [ 'appdata' , 'storage' , 'tools' ] } ,
706+ automation : { contains : [ 'homeassistant' , 'node red' , 'n8n' , 'mqtt' , 'esphome' , 'zigbee' , 'zwave' ] } ,
707+ database : { contains : [ 'postgres' , 'mysql' , 'mariadb' , 'mongo' , 'redis' , 'database' , 'db' ] } ,
708+ security : { contains : [ 'clamav' , 'antivirus' , 'vaultwarden' , 'authentik' , 'authelia' , 'crowdsec' , 'fail2ban' , 'security' ] } ,
709+ development : { contains : [ 'git' , 'code' , 'dev' , 'build' , 'registry' , 'runner' , 'vscode' ] } ,
710+ 'ci-cd' : { contains : [ 'jenkins' , 'runner' , 'drone' , 'argocd' , 'ci' , 'cd' ] } ,
711+ gaming : { contains : [ 'crafty' , 'minecraft' , 'palworld' , 'valheim' , 'satisfactory' , 'steam' , 'gameserver' , 'server' ] } ,
712+ 'game-servers' : { contains : [ 'crafty' , 'pterodactyl' , 'pelican' , 'steamcmd' , 'minecraft' , 'palworld' , 'valheim' , 'satisfactory' , 'terraria' , 'enshrouded' , 'gameserver' , 'server' ] }
713+ } ;
714+ return dockerMap [ normalizedName ] || null ;
715+ }
716+ const vmMap = {
717+ 'utility-vms' : { contains : [ 'utility' , 'tools' , 'helper' , 'management' , 'admin' ] } ,
718+ 'network-vms' : { contains : [ 'router' , 'firewall' , 'pfsense' , 'opnsense' , 'dns' , 'proxy' ] } ,
719+ 'security-vms' : { contains : [ 'security' , 'siem' , 'wazuh' , 'ids' , 'ips' , 'firewall' ] } ,
720+ 'desktop-vms' : { contains : [ 'desktop' , 'workstation' , 'windows' , 'ubuntu' , 'macos' ] } ,
721+ 'gaming-vms' : { contains : [ 'gaming' , 'steam' , 'parsec' , 'moonlight' , 'sunshine' , 'gpu' ] }
722+ } ;
723+ return vmMap [ normalizedName ] || null ;
724+ } ;
725+
690726const collectSetupAssistantItemMatchProfile = ( type , itemName , itemInfo ) => {
691727 const resolvedType = normalizeManagedType ( type ) ;
692728 const tokenSet = new Set ( ) ;
@@ -723,6 +759,12 @@ const collectSetupAssistantItemMatchProfile = (type, itemName, itemInfo) => {
723759 addTokens ( itemInfo ?. info ?. Support ) ;
724760 addTokens ( itemInfo ?. info ?. ReadMe ) ;
725761 addTokens ( itemInfo ?. info ?. template ?. path ) ;
762+ ( Array . isArray ( itemInfo ?. info ?. HostConfig ?. Binds ) ? itemInfo . info . HostConfig . Binds : [ ] ) . forEach ( ( bind ) => addTokens ( bind ) ) ;
763+ ( Array . isArray ( itemInfo ?. Mounts ) ? itemInfo . Mounts : [ ] ) . concat ( Array . isArray ( itemInfo ?. info ?. Mounts ) ? itemInfo . info . Mounts : [ ] ) . forEach ( ( mount ) => {
764+ addTokens ( mount ?. Source ) ;
765+ addTokens ( mount ?. Destination ) ;
766+ addTokens ( mount ?. Name ) ;
767+ } ) ;
726768 if ( utils && typeof utils . getComposeProjectFromLabels === 'function' ) {
727769 addTokens ( utils . getComposeProjectFromLabels ( labels ) ) ;
728770 }
@@ -743,7 +785,7 @@ const collectSetupAssistantItemMatchProfile = (type, itemName, itemInfo) => {
743785 } ;
744786} ;
745787
746- const scoreSetupAssistantTemplateMatch = ( profile , blueprint ) => {
788+ const scoreSetupAssistantTemplateMatch = ( profile , blueprint , type = 'docker' ) => {
747789 const tokens = profile instanceof Set ? profile : profile ?. tokens ;
748790 const phrases = profile ?. phrases instanceof Set ? profile . phrases : new Set ( ) ;
749791 const normalizedText = String ( profile ?. normalizedText || '' ) . trim ( ) ;
@@ -773,6 +815,15 @@ const scoreSetupAssistantTemplateMatch = (profile, blueprint) => {
773815 parts . forEach ( ( part ) => {
774816 if ( tokens . has ( part ) ) {
775817 matchedParts += 1 ;
818+ return ;
819+ }
820+ if ( part . length >= 4 ) {
821+ for ( const token of tokens ) {
822+ if ( token . includes ( part ) || part . includes ( token ) ) {
823+ matchedParts += 1 ;
824+ break ;
825+ }
826+ }
776827 }
777828 } ) ;
778829 if ( matchedParts === parts . length ) {
@@ -782,8 +833,50 @@ const scoreSetupAssistantTemplateMatch = (profile, blueprint) => {
782833 }
783834 score += keywordScore ;
784835 } ;
836+ const hasTokenEndingWith = ( suffix ) => {
837+ const normalizedSuffix = String ( suffix || '' ) . trim ( ) . toLowerCase ( ) ;
838+ if ( ! normalizedSuffix ) {
839+ return false ;
840+ }
841+ for ( const token of tokens ) {
842+ if ( token . endsWith ( normalizedSuffix ) ) {
843+ return true ;
844+ }
845+ }
846+ return false ;
847+ } ;
848+ const hasTokenContainingAny = ( values = [ ] ) => {
849+ const safeValues = Array . isArray ( values ) ? values : [ ] ;
850+ for ( const rawValue of safeValues ) {
851+ const value = normalizeSetupAssistantMatchText ( rawValue ) ;
852+ if ( ! value ) {
853+ continue ;
854+ }
855+ for ( const token of tokens ) {
856+ if ( token === value || token . includes ( value ) || value . includes ( token ) ) {
857+ return true ;
858+ }
859+ }
860+ if ( normalizedText . includes ( value ) ) {
861+ return true ;
862+ }
863+ }
864+ return false ;
865+ } ;
785866 detectKeywords . forEach ( ( keyword ) => consumeKeyword ( keyword , 1 ) ) ;
786867 consumeKeyword ( blueprint . name , 0.6 ) ;
868+ const heuristic = getSetupAssistantBlueprintHeuristicMap ( type , blueprint ?. name || '' ) ;
869+ if ( heuristic ) {
870+ if ( normalizeSetupAssistantMatchText ( blueprint ?. name ) . replace ( / \s + / g, '-' ) === 'media' && hasTokenEndingWith ( 'arr' ) ) {
871+ score += 12 ;
872+ }
873+ if ( hasTokenContainingAny ( heuristic . contains ) ) {
874+ score += 10 ;
875+ }
876+ if ( hasTokenContainingAny ( heuristic . pathContains ) ) {
877+ score += 6 ;
878+ }
879+ }
787880 return score ;
788881} ;
789882
@@ -799,6 +892,8 @@ const buildSetupAssistantTemplateAssignmentPreview = (type, selectedBlueprints)
799892 assignedByTemplate [ name ] = [ ] ;
800893 }
801894 } ) ;
895+ const fallbackTemplateName = String ( SETUP_ASSISTANT_TEMPLATE_FALLBACK_BY_TYPE [ resolvedType ] || '' ) . trim ( ) ;
896+ const fallbackBlueprint = blueprints . find ( ( blueprint ) => String ( blueprint ?. name || '' ) . trim ( ) === fallbackTemplateName ) || null ;
802897
803898 let matched = 0 ;
804899 let unmatched = 0 ;
@@ -807,12 +902,16 @@ const buildSetupAssistantTemplateAssignmentPreview = (type, selectedBlueprints)
807902 let bestBlueprint = null ;
808903 let bestScore = 0 ;
809904 blueprints . forEach ( ( blueprint ) => {
810- const score = scoreSetupAssistantTemplateMatch ( profile , blueprint ) ;
905+ const score = scoreSetupAssistantTemplateMatch ( profile , blueprint , resolvedType ) ;
811906 if ( score > bestScore ) {
812907 bestScore = score ;
813908 bestBlueprint = blueprint ;
814909 }
815910 } ) ;
911+ if ( ( ! bestBlueprint || bestScore < 4 ) && fallbackBlueprint ) {
912+ bestBlueprint = fallbackBlueprint ;
913+ bestScore = 4 ;
914+ }
816915 if ( ! bestBlueprint || bestScore < 4 ) {
817916 unmatched += 1 ;
818917 return ;
@@ -842,7 +941,18 @@ const buildSetupAssistantTemplatePlanForType = (type) => {
842941 const bootstrap = getSetupAssistantTemplateBootstrap ( resolvedType ) ;
843942 const categoryState = getSetupAssistantTemplateCategoryState ( resolvedType ) ;
844943 const selectedNames = new Set ( serializeSetupAssistantTemplateSelections ( bootstrap . selectedTemplateNames ) ) ;
845- const selectedBlueprints = categoryState . blueprints . filter ( ( entry ) => selectedNames . has ( String ( entry ?. name || '' ) . trim ( ) ) ) ;
944+ let selectedBlueprints = categoryState . blueprints . filter ( ( entry ) => selectedNames . has ( String ( entry ?. name || '' ) . trim ( ) ) ) ;
945+ if ( bootstrap . enabled === true && bootstrap . autoAssignExisting === true && bootstrap . category === 'smart' && selectedBlueprints . length > 0 ) {
946+ const fallbackName = String ( SETUP_ASSISTANT_TEMPLATE_FALLBACK_BY_TYPE [ resolvedType ] || '' ) . trim ( ) ;
947+ const fallbackBlueprint = categoryState . blueprints . find ( ( entry ) => String ( entry ?. name || '' ) . trim ( ) === fallbackName ) || null ;
948+ const hasFallbackSelected = fallbackBlueprint && selectedBlueprints . some ( ( entry ) => String ( entry ?. name || '' ) . trim ( ) === fallbackName ) ;
949+ if ( fallbackBlueprint && ! hasFallbackSelected ) {
950+ const previewWithoutFallback = buildSetupAssistantTemplateAssignmentPreview ( resolvedType , selectedBlueprints ) ;
951+ if ( ( previewWithoutFallback . unmatched || 0 ) > 0 ) {
952+ selectedBlueprints = [ ...selectedBlueprints , fallbackBlueprint ] ;
953+ }
954+ }
955+ }
846956 const existingFolders = getFolderMap ( resolvedType ) ;
847957 const existingNameSet = new Set (
848958 Object . values ( existingFolders || { } )
0 commit comments