From 5d429594aa98a7a9a03e48eae6f2087fc9180b23 Mon Sep 17 00:00:00 2001 From: Chris Sherwood Date: Thu, 16 Jul 2026 11:34:30 -0700 Subject: [PATCH] fix(easy-setup): streamline wizard + robust model recommendations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 1: remove the "Additional Tools" (Notes/Data Tools) section from onboarding and point users to Supply Depot, the browsable app catalog, for everything beyond the three core capabilities. Step 2: add a note that individual countries and a full global map can be installed any time from the Maps Manager. Step 4: default the KB auto-index policy to "Ask me first" (Manual) instead of "Yes, always" — auto-indexing has cost/resource implications a non-technical user won't anticipate from the toggle alone. ollama_service: the recommended-models fallback only fired on a null result, so a successful-but-empty upstream response (models: []) showed "No recommended AI models available" and poisoned the 24h cache. Now the fallback fires on empty too, empty results are never cached, empty caches are ignored, and the upstream request has a 10s timeout. Co-Authored-By: Claude Opus 4.8 (1M context) --- admin/app/services/ollama_service.ts | 19 +++- admin/inertia/pages/easy-setup/index.tsx | 120 +++++++++++------------ 2 files changed, 72 insertions(+), 67 deletions(-) diff --git a/admin/app/services/ollama_service.ts b/admin/app/services/ollama_service.ts index be319e9..174ffd5 100644 --- a/admin/app/services/ollama_service.ts +++ b/admin/app/services/ollama_service.ts @@ -764,7 +764,7 @@ export class OllamaService { ): Promise<{ models: NomadOllamaModel[]; hasMore: boolean } | null> { try { const models = await this.retrieveAndRefreshModels(sort, force) - if (!models) { + if (!models || models.length === 0) { logger.warn( '[OllamaService] Returning fallback recommended models due to failure in fetching available models' ) @@ -819,7 +819,10 @@ export class OllamaService { try { if (!force) { const cachedModels = await this.readModelsFromCache() - if (cachedModels) { + // An empty cached array (e.g. written from a transient empty upstream + // response) must not be treated as valid data — fall through to a + // fresh fetch and, failing that, the fallback list. + if (cachedModels && cachedModels.length > 0) { logger.info('[OllamaService] Using cached available models data') return this.sortModels(cachedModels, sort) } @@ -832,7 +835,7 @@ export class OllamaService { const baseUrl = env.get('NOMAD_API_URL') || NOMAD_API_DEFAULT_BASE_URL const fullUrl = new URL(NOMAD_MODELS_API_PATH, baseUrl).toString() - const response = await axios.get(fullUrl) + const response = await axios.get(fullUrl, { timeout: 10000 }) if (!response.data || !Array.isArray(response.data.models)) { logger.warn( `[OllamaService] Invalid response format when fetching available models: ${JSON.stringify(response.data)}` @@ -849,6 +852,16 @@ export class OllamaService { })) .filter((model) => model.tags.length > 0) + // A successful-but-empty upstream response (0 models, or all filtered out + // as cloud-only) is a soft failure: return null so the caller serves the + // fallback list, and don't poison the 24h cache with an empty array. + if (noCloud.length === 0) { + logger.warn( + '[OllamaService] Nomad API returned no usable (non-cloud) models; using fallback' + ) + return null + } + await this.writeModelsToCache(noCloud) return this.sortModels(noCloud, sort) } catch (error) { diff --git a/admin/inertia/pages/easy-setup/index.tsx b/admin/inertia/pages/easy-setup/index.tsx index da97cf8..89515e8 100644 --- a/admin/inertia/pages/easy-setup/index.tsx +++ b/admin/inertia/pages/easy-setup/index.tsx @@ -11,7 +11,7 @@ import TierSelectionModal from '~/components/TierSelectionModal' import WikipediaSelector from '~/components/WikipediaSelector' import LoadingSpinner from '~/components/LoadingSpinner' import Alert from '~/components/Alert' -import { IconCheck, IconChevronDown, IconChevronUp, IconCpu, IconBooks } from '@tabler/icons-react' +import { IconCheck, IconCpu, IconBooks } from '@tabler/icons-react' import StorageProjectionBar from '~/components/StorageProjectionBar' import { useNotifications } from '~/context/NotificationContext' import useInternetStatus from '~/hooks/useInternetStatus' @@ -81,30 +81,10 @@ function buildCoreCapabilities(aiAssistantName: string): Capability[] { ] } -const ADDITIONAL_TOOLS: Capability[] = [ - { - id: 'notes', - name: 'Notes', - technicalName: 'FlatNotes', - description: 'Simple note-taking app with local storage', - features: ['Markdown support', 'All notes stored locally', 'No account required'], - services: [SERVICE_NAMES.FLATNOTES], - icon: 'IconNotes', - }, - { - id: 'datatools', - name: 'Data Tools', - technicalName: 'CyberChef', - description: 'Swiss Army knife for data encoding, encryption, and analysis', - features: [ - 'Encode/decode data (Base64, hex, etc.)', - 'Encryption and hashing tools', - 'Data format conversion', - ], - services: [SERVICE_NAMES.CYBERCHEF], - icon: 'IconChefHat', - }, -] +// Additional tools (Notes, Data Tools, and the rest of the catalog) are no +// longer surfaced in onboarding — they live in Supply Depot, where the full +// app catalog is browsable any time. Step 1 keeps the focus on the three core +// capabilities and points users to Supply Depot for everything else. type WizardStep = 1 | 2 | 3 | 4 | 5 @@ -123,14 +103,14 @@ export default function EasySetupWizard(props: { const [selectedMapCollections, setSelectedMapCollections] = useState([]) const [selectedAiModels, setSelectedAiModels] = useState([]) // Auto-index policy for the AI Assistant Knowledge Base. Defaults to - // 'Always' so a new user who keeps the default behavior gets the "just - // works" experience — downloads become searchable automatically. Persisted - // to KVStore['rag.defaultIngestPolicy'] on wizard submit (same key #894's - // KB modal toggle reads/writes) so the JIT prompt at first chat sees a - // decided policy and doesn't ask again. - const [ingestPolicy, setIngestPolicy] = useState<'Always' | 'Manual'>('Always') + // 'Manual' ("Ask me first"): auto-indexing has real cost and resource + // implications a non-technical user won't anticipate from the toggle alone, + // so we default to the safe choice and let them opt in. Persisted to + // KVStore['rag.defaultIngestPolicy'] on wizard submit (same key #894's KB + // modal toggle reads/writes) so the JIT prompt at first chat sees a decided + // policy and doesn't ask again. + const [ingestPolicy, setIngestPolicy] = useState<'Always' | 'Manual'>('Manual') const [isProcessing, setIsProcessing] = useState(false) - const [showAdditionalTools, setShowAdditionalTools] = useState(false) const [remoteOllamaEnabled, setRemoteOllamaEnabled] = useState( () => !!props.system.remoteOllamaUrl ) @@ -591,7 +571,7 @@ export default function EasySetupWizard(props: { if (capability.id === 'ai' && isSelected) { const hasAiSelections = selectedAiModels.length > 0 || - ingestPolicy !== 'Always' || + ingestPolicy !== 'Manual' || remoteOllamaEnabled if (hasAiSelections) { const confirmed = window.confirm( @@ -600,7 +580,7 @@ export default function EasySetupWizard(props: { if (!confirmed) return } setSelectedAiModels([]) - setIngestPolicy('Always') + setIngestPolicy('Manual') setRemoteOllamaEnabled(false) setRemoteOllamaUrl('') setRemoteOllamaUrlError(null) @@ -723,13 +703,11 @@ export default function EasySetupWizard(props: { const renderStep1 = () => { // Show all capabilities that exist in the system (including installed ones) const existingCoreCapabilities = CORE_CAPABILITIES.filter(capabilityExists) - const existingAdditionalTools = ADDITIONAL_TOOLS.filter(capabilityExists) - // Check if ALL capabilities are already installed (nothing left to install) - const allCoreInstalled = existingCoreCapabilities.every(isCapabilityInstalled) - const allAdditionalInstalled = existingAdditionalTools.every(isCapabilityInstalled) + // Check if ALL core capabilities are already installed (nothing left to install) const allInstalled = - allCoreInstalled && allAdditionalInstalled && existingCoreCapabilities.length > 0 + existingCoreCapabilities.length > 0 && + existingCoreCapabilities.every(isCapabilityInstalled) return (
@@ -811,29 +789,29 @@ export default function EasySetupWizard(props: {
)} - {/* Additional Tools - Collapsible */} - {existingAdditionalTools.length > 0 && ( -
- - {showAdditionalTools && ( -
- {existingAdditionalTools.map((capability) => - renderCapabilityCard(capability, false) - )} -
- )} + Open Supply Depot +
- )} + )} @@ -849,6 +827,20 @@ export default function EasySetupWizard(props: { regions later.

+
+

+ Only need a specific country, or want the whole world? Individual countries and a full + global map can be installed any time from the{' '} + + . +

+
{isLoadingMaps ? (
@@ -1161,9 +1153,9 @@ export default function EasySetupWizard(props: { Capabilities to Install
    - {[...CORE_CAPABILITIES, ...ADDITIONAL_TOOLS] - .filter((cap) => cap.services.some((s) => selectedServices.includes(s))) - .map((capability) => ( + {CORE_CAPABILITIES.filter((cap) => + cap.services.some((s) => selectedServices.includes(s)) + ).map((capability) => (
  • @@ -1353,7 +1345,7 @@ export default function EasySetupWizard(props: {

    {(() => { - const count = [...CORE_CAPABILITIES, ...ADDITIONAL_TOOLS].filter((cap) => + const count = CORE_CAPABILITIES.filter((cap) => cap.services.some((s) => selectedServices.includes(s)) ).length return `${count} ${count === 1 ? 'capability' : 'capabilities'}`