@@ -603,7 +603,7 @@ const getSetupAssistantTemplateCategoryState = (type) => {
603603 categoryIndexBuckets . get ( normalized ) . add ( index ) ;
604604 } ;
605605
606- const smartIndexes = resolveStarterTemplateSmartIndexes ( resolvedType , blueprints ) ;
606+ const smartIndexes = resolveSetupAssistantSmartBlueprintIndexes ( resolvedType , blueprints ) . indexes ;
607607 smartIndexes . forEach ( ( index ) => addCategoryIndex ( 'smart' , index ) ) ;
608608 blueprints . forEach ( ( entry , index ) => {
609609 const categories = Array . isArray ( entry ?. categories ) ? entry . categories : [ ] ;
@@ -691,37 +691,7 @@ const SETUP_ASSISTANT_TEMPLATE_FALLBACK_BY_TYPE = Object.freeze({
691691 docker : 'Utilities' ,
692692 vm : 'Utility VMs'
693693} ) ;
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- } ;
694+ const SETUP_ASSISTANT_TEMPLATE_MATCH_THRESHOLD = 4 ;
725695
726696const collectSetupAssistantItemMatchProfile = ( type , itemName , itemInfo ) => {
727697 const resolvedType = normalizeManagedType ( type ) ;
@@ -863,21 +833,103 @@ const scoreSetupAssistantTemplateMatch = (profile, blueprint, type = 'docker') =
863833 }
864834 return false ;
865835 } ;
836+ const getHeuristicBoost = ( ) => {
837+ const normalizedName = normalizeSetupAssistantMatchText ( blueprint ?. name ) . replace ( / \s + / g, '-' ) ;
838+ if ( type !== 'docker' ) {
839+ return 0 ;
840+ }
841+ if ( normalizedName === 'media' ) {
842+ let next = 0 ;
843+ if ( hasTokenEndingWith ( 'arr' ) ) {
844+ next += 12 ;
845+ }
846+ if ( hasTokenContainingAny ( [ 'seerr' , 'wizarr' , 'listenarr' , 'cleanuparr' , 'agregarr' , 'watch' , 'request' , 'discover' ] ) ) {
847+ next += 10 ;
848+ }
849+ if ( hasTokenContainingAny ( [ 'media' , 'movies' , 'shows' , 'tv' , 'music' , 'books' , 'audiobooks' , 'anime' , 'comics' , 'photos' ] ) ) {
850+ next += 6 ;
851+ }
852+ return next ;
853+ }
854+ if ( normalizedName === 'game-servers' || normalizedName === 'gaming' ) {
855+ return hasTokenContainingAny ( [ 'crafty' , 'pterodactyl' , 'pelican' , 'satisfactory' , 'minecraft' , 'palworld' , 'valheim' , 'steamcmd' , 'gameserver' , 'server' ] ) ? 10 : 0 ;
856+ }
857+ if ( normalizedName === 'monitoring' ) {
858+ return hasTokenContainingAny ( [ 'myspeed' , 'speedtest' , 'latency' , 'uptime' , 'metrics' ] ) ? 10 : 0 ;
859+ }
860+ if ( normalizedName === 'cloud-&-sync' ) {
861+ return hasTokenContainingAny ( [ 'nextcloud' , 'owncloud' , 'seafile' , 'cloud' , 'sync' , 'drive' , 'collabora' , 'onlyoffice' ] ) ? 10 : 0 ;
862+ }
863+ if ( normalizedName === 'notifications' ) {
864+ return hasTokenContainingAny ( [ 'notify' , 'notification' , 'ntfy' , 'gotify' , 'apprise' , 'notifiarr' , 'pushover' , 'webhook' ] ) ? 10 : 0 ;
865+ }
866+ if ( normalizedName === 'utilities' ) {
867+ let next = hasTokenContainingAny ( [ 'qdirstat' , 'diskspeed' , 'icons' , 'icon' , 'tool' , 'utility' , 'manager' ] ) ? 10 : 0 ;
868+ if ( hasTokenContainingAny ( [ 'appdata' , 'storage' , 'tools' ] ) ) {
869+ next += 6 ;
870+ }
871+ return next ;
872+ }
873+ if ( normalizedName === 'security' ) {
874+ return hasTokenContainingAny ( [ 'clamav' , 'antivirus' , 'vaultwarden' , 'authentik' , 'authelia' , 'crowdsec' , 'fail2ban' , 'security' ] ) ? 10 : 0 ;
875+ }
876+ return 0 ;
877+ } ;
866878 detectKeywords . forEach ( ( keyword ) => consumeKeyword ( keyword , 1 ) ) ;
867879 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 ;
880+ score += getHeuristicBoost ( ) ;
881+ return score ;
882+ } ;
883+
884+ const resolveSetupAssistantTemplateBestMatch = ( type , profile , blueprints ) => {
885+ const resolvedType = normalizeManagedType ( type ) ;
886+ let bestBlueprint = null ;
887+ let bestScore = 0 ;
888+ let bestIndex = - 1 ;
889+ ( Array . isArray ( blueprints ) ? blueprints : [ ] ) . forEach ( ( blueprint , index ) => {
890+ const score = scoreSetupAssistantTemplateMatch ( profile , blueprint , resolvedType ) ;
891+ if ( score > bestScore ) {
892+ bestBlueprint = blueprint ;
893+ bestScore = score ;
894+ bestIndex = index ;
872895 }
873- if ( hasTokenContainingAny ( heuristic . contains ) ) {
874- score += 10 ;
896+ } ) ;
897+ return { bestBlueprint, bestScore, bestIndex } ;
898+ } ;
899+
900+ const resolveSetupAssistantSmartBlueprintIndexes = ( type , blueprints ) => {
901+ const resolvedType = normalizeManagedType ( type ) ;
902+ const safeBlueprints = Array . isArray ( blueprints ) ? blueprints : [ ] ;
903+ const itemNames = getBulkAssignableNames ( resolvedType ) ;
904+ const info = infoByType [ resolvedType ] || { } ;
905+ const matchedIndexes = new Set ( ) ;
906+ const fallbackTemplateName = String ( SETUP_ASSISTANT_TEMPLATE_FALLBACK_BY_TYPE [ resolvedType ] || '' ) . trim ( ) ;
907+ const fallbackIndex = safeBlueprints . findIndex ( ( blueprint ) => String ( blueprint ?. name || '' ) . trim ( ) === fallbackTemplateName ) ;
908+
909+ let matched = 0 ;
910+ let unmatched = 0 ;
911+ itemNames . forEach ( ( itemName ) => {
912+ const profile = collectSetupAssistantItemMatchProfile ( resolvedType , itemName , info [ itemName ] || { } ) ;
913+ const { bestScore, bestIndex } = resolveSetupAssistantTemplateBestMatch ( resolvedType , profile , safeBlueprints ) ;
914+ if ( bestIndex >= 0 && bestScore >= SETUP_ASSISTANT_TEMPLATE_MATCH_THRESHOLD ) {
915+ matchedIndexes . add ( bestIndex ) ;
916+ matched += 1 ;
917+ return ;
875918 }
876- if ( hasTokenContainingAny ( heuristic . pathContains ) ) {
877- score += 6 ;
919+ if ( fallbackIndex >= 0 ) {
920+ matchedIndexes . add ( fallbackIndex ) ;
921+ matched += 1 ;
922+ return ;
878923 }
879- }
880- return score ;
924+ unmatched += 1 ;
925+ } ) ;
926+
927+ return {
928+ indexes : matchedIndexes ,
929+ totalItems : itemNames . length ,
930+ matched,
931+ unmatched
932+ } ;
881933} ;
882934
883935const buildSetupAssistantTemplateAssignmentPreview = ( type , selectedBlueprints ) => {
@@ -899,20 +951,12 @@ const buildSetupAssistantTemplateAssignmentPreview = (type, selectedBlueprints)
899951 let unmatched = 0 ;
900952 itemNames . forEach ( ( itemName ) => {
901953 const profile = collectSetupAssistantItemMatchProfile ( resolvedType , itemName , info [ itemName ] || { } ) ;
902- let bestBlueprint = null ;
903- let bestScore = 0 ;
904- blueprints . forEach ( ( blueprint ) => {
905- const score = scoreSetupAssistantTemplateMatch ( profile , blueprint , resolvedType ) ;
906- if ( score > bestScore ) {
907- bestScore = score ;
908- bestBlueprint = blueprint ;
909- }
910- } ) ;
911- if ( ( ! bestBlueprint || bestScore < 4 ) && fallbackBlueprint ) {
954+ let { bestBlueprint, bestScore } = resolveSetupAssistantTemplateBestMatch ( resolvedType , profile , blueprints ) ;
955+ if ( ( ! bestBlueprint || bestScore < SETUP_ASSISTANT_TEMPLATE_MATCH_THRESHOLD ) && fallbackBlueprint ) {
912956 bestBlueprint = fallbackBlueprint ;
913- bestScore = 4 ;
957+ bestScore = SETUP_ASSISTANT_TEMPLATE_MATCH_THRESHOLD ;
914958 }
915- if ( ! bestBlueprint || bestScore < 4 ) {
959+ if ( ! bestBlueprint || bestScore < SETUP_ASSISTANT_TEMPLATE_MATCH_THRESHOLD ) {
916960 unmatched += 1 ;
917961 return ;
918962 }
0 commit comments