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'}`