fix: service name defs and ollama ui location

This commit is contained in:
Jake Turner 2026-02-01 05:46:23 +00:00
parent 4584844ca6
commit 31c671bdb5
15 changed files with 62 additions and 44 deletions

View File

@ -24,6 +24,7 @@ import type {
} from '../../types/benchmark.js'
import { randomUUID, createHmac } from 'node:crypto'
import { DockerService } from './docker_service.js'
import { SERVICE_NAMES } from '../../constants/service_names.js'
// HMAC secret for signing submissions to the benchmark repository
// This provides basic protection against casual API abuse.
@ -421,7 +422,7 @@ export class BenchmarkService {
this._updateStatus('running_ai', 'Running AI benchmark...')
const ollamaAPIURL = await this.dockerService.getServiceURL(DockerService.OLLAMA_SERVICE_NAME)
const ollamaAPIURL = await this.dockerService.getServiceURL(SERVICE_NAMES.OLLAMA)
if (!ollamaAPIURL) {
throw new Error('AI Assistant service location could not be determined. Ensure AI Assistant is installed and running.')
}

View File

@ -6,18 +6,12 @@ import transmit from '@adonisjs/transmit/services/main'
import { doResumableDownloadWithRetry } from '../utils/downloads.js'
import { join } from 'path'
import { ZIM_STORAGE_PATH } from '../utils/fs.js'
import { SERVICE_NAMES } from '../../constants/service_names.js'
@inject()
export class DockerService {
public docker: Docker
private activeInstallations: Set<string> = new Set()
public static KIWIX_SERVICE_NAME = 'nomad_kiwix_serve'
public static OLLAMA_SERVICE_NAME = 'nomad_ollama'
public static QDRANT_SERVICE_NAME = 'nomad_qdrant'
public static CYBERCHEF_SERVICE_NAME = 'nomad_cyberchef'
public static FLATNOTES_SERVICE_NAME = 'nomad_flatnotes'
public static KOLIBRI_SERVICE_NAME = 'nomad_kolibri'
public static BENCHMARK_SERVICE_NAME = 'nomad_benchmark'
public static NOMAD_NETWORK = 'project-nomad_default'
constructor() {
@ -441,7 +435,7 @@ export class DockerService {
await new Promise((res) => this.docker.modem.followProgress(pullStream, res))
}
if (service.service_name === DockerService.KIWIX_SERVICE_NAME) {
if (service.service_name === SERVICE_NAMES.KIWIX) {
await this._runPreinstallActions__KiwixServe()
this._broadcast(
service.service_name,
@ -556,12 +550,12 @@ export class DockerService {
logger.info(`[DockerService] Kiwix Serve pre-install: Downloading ZIM file to ${filepath}`)
this._broadcast(
DockerService.KIWIX_SERVICE_NAME,
SERVICE_NAMES.KIWIX,
'preinstall',
`Running pre-install actions for Kiwix Serve...`
)
this._broadcast(
DockerService.KIWIX_SERVICE_NAME,
SERVICE_NAMES.KIWIX,
'preinstall',
`Downloading Wikipedia ZIM file from ${WIKIPEDIA_ZIM_URL}. This may take some time...`
)
@ -579,13 +573,13 @@ export class DockerService {
})
this._broadcast(
DockerService.KIWIX_SERVICE_NAME,
SERVICE_NAMES.KIWIX,
'preinstall',
`Downloaded Wikipedia ZIM file to ${filepath}`
)
} catch (error) {
this._broadcast(
DockerService.KIWIX_SERVICE_NAME,
SERVICE_NAMES.KIWIX,
'preinstall-error',
`Failed to download Wikipedia ZIM file: ${error.message}`
)

View File

@ -1,6 +1,5 @@
import { inject } from '@adonisjs/core'
import { ChatRequest, Ollama } from 'ollama'
import { DockerService } from './docker_service.js'
import { NomadOllamaModel } from '../../types/ollama.js'
import { FALLBACK_RECOMMENDED_OLLAMA_MODELS } from '../../constants/ollama.js'
import fs from 'node:fs/promises'
@ -9,6 +8,7 @@ import logger from '@adonisjs/core/services/logger'
import axios from 'axios'
import { DownloadModelJob } from '#jobs/download_model_job'
import { PassThrough } from 'node:stream'
import { SERVICE_NAMES } from '../../constants/service_names.js'
const NOMAD_MODELS_API_BASE_URL = 'https://api.projectnomad.us/api/v1/ollama/models'
const MODELS_CACHE_FILE = path.join(process.cwd(), 'storage', 'ollama-models-cache.json')
@ -25,7 +25,7 @@ export class OllamaService {
if (!this.ollamaInitPromise) {
this.ollamaInitPromise = (async () => {
const dockerService = new (await import('./docker_service.js')).DockerService()
const qdrantUrl = await dockerService.getServiceURL(DockerService.OLLAMA_SERVICE_NAME)
const qdrantUrl = await dockerService.getServiceURL(SERVICE_NAMES.OLLAMA)
if (!qdrantUrl) {
throw new Error('Ollama service is not installed or running.')
}
@ -56,7 +56,7 @@ export class OllamaService {
return new Promise(async (resolve) => {
try {
const dockerService = new (await import('./docker_service.js')).DockerService()
const container = dockerService.docker.getContainer(DockerService.OLLAMA_SERVICE_NAME)
const container = dockerService.docker.getContainer(SERVICE_NAMES.OLLAMA)
if (!container) {
logger.warn('[OllamaService] Ollama container is not running. Cannot download model.')
resolve({
@ -242,7 +242,7 @@ export class OllamaService {
const dockerService = new (await import('./docker_service.js')).DockerService()
const ollamAPIURL = await dockerService.getServiceURL(DockerService.OLLAMA_SERVICE_NAME)
const ollamAPIURL = await dockerService.getServiceURL(SERVICE_NAMES.OLLAMA)
if (!ollamAPIURL) {
logger.warn('[OllamaService] Ollama service is not running. Cannot download model.')
return {

View File

@ -9,6 +9,7 @@ import { PDFParse } from 'pdf-parse'
import { createWorker } from 'tesseract.js'
import { fromBuffer } from 'pdf2pic'
import { OllamaService } from './ollama_service.js'
import { SERVICE_NAMES } from '../../constants/service_names.js'
@inject()
export class RagService {
@ -26,7 +27,7 @@ export class RagService {
private async _initializeQdrantClient() {
if (!this.qdrantInitPromise) {
this.qdrantInitPromise = (async () => {
const qdrantUrl = await this.dockerService.getServiceURL(DockerService.QDRANT_SERVICE_NAME)
const qdrantUrl = await this.dockerService.getServiceURL(SERVICE_NAMES.QDRANT)
if (!qdrantUrl) {
throw new Error('Qdrant service is not installed or running.')
}

View File

@ -26,6 +26,7 @@ import InstalledTier from '#models/installed_tier'
import WikipediaSelection from '#models/wikipedia_selection'
import { RunDownloadJob } from '#jobs/run_download_job'
import { DownloadCollectionOperation, DownloadRemoteSuccessCallback } from '../../types/files.js'
import { SERVICE_NAMES } from '../../constants/service_names.js'
const ZIM_MIME_TYPES = ['application/x-zim', 'application/x-openzim', 'application/octet-stream']
const CATEGORIES_URL = 'https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/master/collections/kiwix-categories.json'
@ -243,7 +244,7 @@ export class ZimService implements IZimService {
// Restart KIWIX container to pick up new ZIM file
if (restart) {
await this.dockerService
.affectContainer(DockerService.KIWIX_SERVICE_NAME, 'restart')
.affectContainer(SERVICE_NAMES.KIWIX, 'restart')
.catch((error) => {
logger.error(`[ZimService] Failed to restart KIWIX container:`, error) // Don't stop the download completion, just log the error.
})
@ -434,7 +435,7 @@ export class ZimService implements IZimService {
// Restart Kiwix to reflect the change
await this.dockerService
.affectContainer(DockerService.KIWIX_SERVICE_NAME, 'restart')
.affectContainer(SERVICE_NAMES.KIWIX, 'restart')
.catch((error) => {
logger.error(`[ZimService] Failed to restart Kiwix after Wikipedia removal:`, error)
})

View File

@ -0,0 +1,8 @@
export const SERVICE_NAMES = {
KIWIX: 'nomad_kiwix_server',
OLLAMA: 'nomad_ollama',
QDRANT: 'nomad_qdrant',
CYBERCHEF: 'nomad_cyberchef',
FLATNOTES: 'nomad_flatnotes',
KOLIBRI: 'nomad_kolibri',
}

View File

@ -27,6 +27,7 @@ export default class extends BaseSchema {
UPDATE services SET
friendly_name = 'AI Assistant',
powered_by = 'Ollama',
ui_location = '/chat',
display_order = 3,
description = 'Local AI chat that runs entirely on your hardware - no internet required'
WHERE service_name = 'nomad_ollama'

View File

@ -1,8 +1,8 @@
import Service from '#models/service'
import { DockerService } from '#services/docker_service'
import { BaseSeeder } from '@adonisjs/lucid/seeders'
import { ModelAttributes } from '@adonisjs/lucid/types/model'
import env from '#start/env'
import { SERVICE_NAMES } from '../../constants/service_names.js'
export default class ServiceSeeder extends BaseSeeder {
// Use environment variable with fallback to production default
@ -15,7 +15,7 @@ export default class ServiceSeeder extends BaseSeeder {
'created_at' | 'updated_at' | 'metadata' | 'id'
>[] = [
{
service_name: DockerService.KIWIX_SERVICE_NAME,
service_name: SERVICE_NAMES.KIWIX,
friendly_name: 'Information Library',
powered_by: 'Kiwix',
display_order: 1,
@ -39,7 +39,7 @@ export default class ServiceSeeder extends BaseSeeder {
depends_on: null,
},
{
service_name: DockerService.QDRANT_SERVICE_NAME,
service_name: SERVICE_NAMES.QDRANT,
friendly_name: 'Qdrant Vector Database',
powered_by: null,
display_order: 100, // Dependency service, not shown directly
@ -62,7 +62,7 @@ export default class ServiceSeeder extends BaseSeeder {
depends_on: null,
},
{
service_name: DockerService.OLLAMA_SERVICE_NAME,
service_name: SERVICE_NAMES.OLLAMA,
friendly_name: 'AI Assistant',
powered_by: 'Ollama',
display_order: 3,
@ -78,14 +78,14 @@ export default class ServiceSeeder extends BaseSeeder {
},
ExposedPorts: { '11434/tcp': {} },
}),
ui_location: null,
ui_location: '/chat',
installed: false,
installation_status: 'idle',
is_dependency_service: false,
depends_on: DockerService.QDRANT_SERVICE_NAME,
depends_on: SERVICE_NAMES.QDRANT,
},
{
service_name: DockerService.CYBERCHEF_SERVICE_NAME,
service_name: SERVICE_NAMES.CYBERCHEF,
friendly_name: 'Data Tools',
powered_by: 'CyberChef',
display_order: 11,
@ -107,7 +107,7 @@ export default class ServiceSeeder extends BaseSeeder {
depends_on: null,
},
{
service_name: DockerService.FLATNOTES_SERVICE_NAME,
service_name: SERVICE_NAMES.FLATNOTES,
friendly_name: 'Notes',
powered_by: 'FlatNotes',
display_order: 10,
@ -131,7 +131,7 @@ export default class ServiceSeeder extends BaseSeeder {
depends_on: null,
},
{
service_name: DockerService.KOLIBRI_SERVICE_NAME,
service_name: SERVICE_NAMES.KOLIBRI,
friendly_name: 'Education Platform',
powered_by: 'Kolibri',
display_order: 2,

View File

@ -16,6 +16,12 @@ export function getServiceLink(ui_location: string): string {
// If it's a port number, return a link to the service on that port
return `http://${window.location.hostname}:${parsedPort}`;
}
// Otherwise, treat it as a path
const pathPattern = /^\/.+/;
if (pathPattern.test(ui_location)) {
// If it starts with a slash, treat it as a full path
return ui_location;
}
return `/${ui_location}`;
}

View File

@ -18,6 +18,7 @@ import useInternetStatus from '~/hooks/useInternetStatus'
import { useSystemInfo } from '~/hooks/useSystemInfo'
import classNames from 'classnames'
import { CuratedCategory, CategoryTier, CategoryResource } from '../../../types/downloads'
import { SERVICE_NAMES } from '../../../constants/service_names'
// Capability definitions - maps user-friendly categories to services
interface Capability {
@ -43,7 +44,7 @@ const CORE_CAPABILITIES: Capability[] = [
'WikiHow articles and tutorials',
'Project Gutenberg books and literature',
],
services: ['nomad_kiwix_serve'],
services: [SERVICE_NAMES.KIWIX],
icon: 'IconBooks',
},
{
@ -57,7 +58,7 @@ const CORE_CAPABILITIES: Capability[] = [
'Interactive exercises and quizzes',
'Progress tracking for learners',
],
services: ['nomad_kolibri'],
services: [SERVICE_NAMES.KOLIBRI],
icon: 'IconSchool',
},
{
@ -71,7 +72,7 @@ const CORE_CAPABILITIES: Capability[] = [
'Ask questions, get help with writing, brainstorm ideas',
'Runs on your own hardware with local AI models',
],
services: ['nomad_ollama'],
services: [SERVICE_NAMES.OLLAMA],
icon: 'IconRobot',
},
]
@ -83,7 +84,7 @@ const ADDITIONAL_TOOLS: Capability[] = [
technicalName: 'FlatNotes',
description: 'Simple note-taking app with local storage',
features: ['Markdown support', 'All notes stored locally', 'No account required'],
services: ['nomad_flatnotes'],
services: [SERVICE_NAMES.FLATNOTES],
icon: 'IconNotes',
},
{
@ -96,7 +97,7 @@ const ADDITIONAL_TOOLS: Capability[] = [
'Encryption and hashing tools',
'Data format conversion',
],
services: ['nomad_cyberchef'],
services: [SERVICE_NAMES.CYBERCHEF],
icon: 'IconChefHat',
},
]
@ -804,10 +805,10 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim
const renderStep3 = () => {
// Check if AI or Information capabilities are selected OR already installed
const isAiSelected = selectedServices.includes('nomad_ollama') ||
installedServices.some((s) => s.service_name === 'nomad_ollama')
const isInformationSelected = selectedServices.includes('nomad_kiwix') ||
installedServices.some((s) => s.service_name === 'nomad_kiwix')
const isAiSelected = selectedServices.includes(SERVICE_NAMES.OLLAMA) ||
installedServices.some((s) => s.service_name === SERVICE_NAMES.OLLAMA)
const isInformationSelected = selectedServices.includes(SERVICE_NAMES.KIWIX) ||
installedServices.some((s) => s.service_name === SERVICE_NAMES.KIWIX)
return (
<div className="space-y-6">

View File

@ -12,6 +12,7 @@ import AppLayout from '~/layouts/AppLayout'
import { getServiceLink } from '~/lib/navigation'
import { ServiceSlim } from '../../types/services'
import DynamicIcon, { DynamicIconName } from '~/components/DynamicIcon'
import { SERVICE_NAMES } from '../../constants/service_names'
// Maps is a Core Capability (display_order: 4)
const MAPS_ITEM = {
@ -126,7 +127,7 @@ export default function Home(props: {
// Add system items
items.push(...SYSTEM_ITEMS)
if (props.system.services.find((s) => s.service_name === 'nomad_ollama' && s.installed)) {
if (props.system.services.find((s) => s.service_name === SERVICE_NAMES.OLLAMA && s.installed)) {
items.push(KNOWLEDGE_BASE_ITEM)
}

View File

@ -22,6 +22,7 @@ import { BenchmarkProgress, BenchmarkStatus } from '../../../types/benchmark'
import BenchmarkResult from '#models/benchmark_result'
import api from '~/lib/api'
import useServiceInstalledStatus from '~/hooks/useServiceInstalledStatus'
import { SERVICE_NAMES } from '../../../constants/service_names'
type BenchmarkProgressWithID = BenchmarkProgress & { benchmark_id: string }
@ -34,7 +35,7 @@ export default function BenchmarkPage(props: {
}) {
const { subscribe } = useTransmit()
const queryClient = useQueryClient()
const aiInstalled = useServiceInstalledStatus('nomad_ollama')
const aiInstalled = useServiceInstalledStatus(SERVICE_NAMES.OLLAMA)
const [progress, setProgress] = useState<BenchmarkProgressWithID | null>(null)
const [isRunning, setIsRunning] = useState(props.benchmark.status !== 'idle')
const [showDetails, setShowDetails] = useState(false)

View File

@ -10,11 +10,12 @@ import api from '~/lib/api'
import { useModals } from '~/context/ModalContext'
import StyledModal from '~/components/StyledModal'
import { ModelResponse } from 'ollama'
import { SERVICE_NAMES } from '../../../constants/service_names'
export default function ModelsPage(props: {
models: { availableModels: NomadOllamaModel[]; installedModels: ModelResponse[] }
}) {
const { isInstalled } = useServiceInstalledStatus('nomad_ollama')
const { isInstalled } = useServiceInstalledStatus(SERVICE_NAMES.OLLAMA)
const { addNotification } = useNotifications()
const { openModal, closeAllModals } = useModals()

View File

@ -9,11 +9,12 @@ import StyledModal from '~/components/StyledModal'
import useServiceInstalledStatus from '~/hooks/useServiceInstalledStatus'
import Alert from '~/components/Alert'
import { FileEntry } from '../../../../types/files'
import { SERVICE_NAMES } from '../../../../constants/service_names'
export default function ZimPage() {
const queryClient = useQueryClient()
const { openModal, closeAllModals } = useModals()
const { isInstalled } = useServiceInstalledStatus('nomad_kiwix_serve')
const { isInstalled } = useServiceInstalledStatus(SERVICE_NAMES.KIWIX)
const { data, isLoading } = useQuery<FileEntry[]>({
queryKey: ['zim-files'],
queryFn: getFiles,

View File

@ -36,6 +36,7 @@ import {
} from '../../../../types/downloads'
import useDownloads from '~/hooks/useDownloads'
import ActiveDownloads from '~/components/ActiveDownloads'
import { SERVICE_NAMES } from '../../../../constants/service_names'
const CURATED_COLLECTIONS_KEY = 'curated-zim-collections'
const CURATED_CATEGORIES_KEY = 'curated-categories'
@ -60,7 +61,7 @@ export default function ZimRemoteExplorer() {
const { openModal, closeAllModals } = useModals()
const { addNotification } = useNotifications()
const { isOnline } = useInternetStatus()
const { isInstalled } = useServiceInstalledStatus('nomad_kiwix_serve')
const { isInstalled } = useServiceInstalledStatus(SERVICE_NAMES.KIWIX)
const { debounce } = useDebounce()
const [query, setQuery] = useState('')