diff --git a/admin/app/controllers/chats_controller.ts b/admin/app/controllers/chats_controller.ts index f6ad4d7..005e60d 100644 --- a/admin/app/controllers/chats_controller.ts +++ b/admin/app/controllers/chats_controller.ts @@ -2,7 +2,6 @@ import { inject } from '@adonisjs/core' import type { HttpContext } from '@adonisjs/core/http' import { ChatService } from '#services/chat_service' import { createSessionSchema, updateSessionSchema, addMessageSchema } from '#validators/chat' -import { parseBoolean } from '../utils/misc.js' import KVStore from '#models/kv_store' import { SystemService } from '#services/system_service' import { SERVICE_NAMES } from '../../constants/service_names.js' @@ -20,7 +19,7 @@ export default class ChatsController { const chatSuggestionsEnabled = await KVStore.getValue('chat.suggestionsEnabled') return inertia.render('chat', { settings: { - chatSuggestionsEnabled: parseBoolean(chatSuggestionsEnabled), + chatSuggestionsEnabled: chatSuggestionsEnabled ?? false, }, }) } diff --git a/admin/app/controllers/settings_controller.ts b/admin/app/controllers/settings_controller.ts index 35f8800..2aebdd8 100644 --- a/admin/app/controllers/settings_controller.ts +++ b/admin/app/controllers/settings_controller.ts @@ -6,7 +6,7 @@ import { SystemService } from '#services/system_service'; import { updateSettingSchema } from '#validators/settings'; import { inject } from '@adonisjs/core'; import type { HttpContext } from '@adonisjs/core/http' -import { parseBoolean } from '../utils/misc.js'; +import type { KVStoreKey } from '../../types/kv_store.js'; @inject() export default class SettingsController { @@ -59,7 +59,7 @@ export default class SettingsController { availableModels: availableModels?.models || [], installedModels: installedModels || [], settings: { - chatSuggestionsEnabled: parseBoolean(chatSuggestionsEnabled) + chatSuggestionsEnabled: chatSuggestionsEnabled ?? false } } }); @@ -98,7 +98,7 @@ export default class SettingsController { async getSetting({ request, response }: HttpContext) { const key = request.qs().key; - const value = await KVStore.getValue(key); + const value = await KVStore.getValue(key as KVStoreKey); return response.status(200).send({ key, value }); } diff --git a/admin/app/jobs/check_update_job.ts b/admin/app/jobs/check_update_job.ts index e4fa59b..046d9c0 100644 --- a/admin/app/jobs/check_update_job.ts +++ b/admin/app/jobs/check_update_job.ts @@ -28,7 +28,7 @@ export class CheckUpdateJob { `[CheckUpdateJob] Update available: ${result.currentVersion} → ${result.latestVersion}` ) } else { - await KVStore.setValue('system.updateAvailable', "false") + await KVStore.setValue('system.updateAvailable', false) logger.info( `[CheckUpdateJob] System is up to date (${result.currentVersion})` ) diff --git a/admin/app/models/kv_store.ts b/admin/app/models/kv_store.ts index b483b3f..caad66f 100644 --- a/admin/app/models/kv_store.ts +++ b/admin/app/models/kv_store.ts @@ -1,6 +1,7 @@ import { DateTime } from 'luxon' import { BaseModel, column, SnakeCaseNamingStrategy } from '@adonisjs/lucid/orm' -import type { KVStoreKey, KVStoreValue } from '../../types/kv_store.js' +import { KV_STORE_SCHEMA, type KVStoreKey, type KVStoreValue } from '../../types/kv_store.js' +import { parseBoolean } from '../utils/misc.js' /** * Generic key-value store model for storing various settings @@ -17,7 +18,7 @@ export default class KVStore extends BaseModel { declare key: KVStoreKey @column() - declare value: KVStoreValue + declare value: string | null @column.dateTime({ autoCreate: true }) declare created_at: DateTime @@ -26,26 +27,25 @@ export default class KVStore extends BaseModel { declare updated_at: DateTime /** - * Get a setting value by key + * Get a setting value by key, automatically deserializing to the correct type. */ - static async getValue(key: KVStoreKey): Promise { + static async getValue(key: K): Promise | null> { const setting = await this.findBy('key', key) if (!setting || setting.value === undefined || setting.value === null) { return null } - if (typeof setting.value === 'string') { - return setting.value - } - return String(setting.value) + const raw = String(setting.value) + return (KV_STORE_SCHEMA[key] === 'boolean' ? parseBoolean(raw) : raw) as KVStoreValue } /** - * Set a setting value by key (creates if not exists) + * Set a setting value by key (creates if not exists), automatically serializing to string. */ - static async setValue(key: KVStoreKey, value: KVStoreValue): Promise { - const setting = await this.firstOrCreate({ key }, { key, value }) - if (setting.value !== value) { - setting.value = value + static async setValue(key: K, value: KVStoreValue): Promise { + const serialized = String(value) + const setting = await this.firstOrCreate({ key }, { key, value: serialized }) + if (setting.value !== serialized) { + setting.value = serialized await setting.save() } return setting diff --git a/admin/app/services/docker_service.ts b/admin/app/services/docker_service.ts index a87ff5b..cc5a00f 100644 --- a/admin/app/services/docker_service.ts +++ b/admin/app/services/docker_service.ts @@ -546,7 +546,7 @@ export class DockerService { // If Ollama was just installed, trigger Nomad docs discovery and embedding if (service.service_name === SERVICE_NAMES.OLLAMA) { logger.info('[DockerService] Ollama installation complete. Default behavior is to not enable chat suggestions.') - await KVStore.setValue('chat.suggestionsEnabled', "false") + await KVStore.setValue('chat.suggestionsEnabled', false) logger.info('[DockerService] Ollama installation complete. Triggering Nomad docs discovery...') diff --git a/admin/app/services/rag_service.ts b/admin/app/services/rag_service.ts index c2a4478..302cbbd 100644 --- a/admin/app/services/rag_service.ts +++ b/admin/app/services/rag_service.ts @@ -14,7 +14,6 @@ import { removeStopwords } from 'stopword' import { randomUUID } from 'node:crypto' import { join } from 'node:path' import KVStore from '#models/kv_store' -import { parseBoolean } from '../utils/misc.js' import { ZIMExtractionService } from './zim_extraction_service.js' import { ZIM_BATCH_SIZE } from '../../constants/zim_extraction.js' @@ -885,7 +884,7 @@ export class RagService { const DOCS_DIR = join(process.cwd(), 'docs') const alreadyEmbeddedRaw = await KVStore.getValue('rag.docsEmbedded') - if (parseBoolean(alreadyEmbeddedRaw) && !force) { + if (alreadyEmbeddedRaw && !force) { logger.info('[RAG] Nomad docs have already been discovered and queued. Skipping.') return { success: true, message: 'Nomad docs have already been discovered and queued. Skipping.' } } @@ -927,7 +926,7 @@ export class RagService { } // Update KV store to mark docs as discovered so we don't redo this unnecessarily - await KVStore.setValue('rag.docsEmbedded', 'true') + await KVStore.setValue('rag.docsEmbedded', true) return { success: true, message: `Nomad docs discovery completed. Dispatched ${filesToEmbed.length} embedding jobs.` } } catch (error) { diff --git a/admin/types/kv_store.ts b/admin/types/kv_store.ts index 7d44ef5..c60adad 100644 --- a/admin/types/kv_store.ts +++ b/admin/types/kv_store.ts @@ -1,3 +1,13 @@ -export type KVStoreKey = 'chat.suggestionsEnabled' | 'rag.docsEmbedded' | 'system.updateAvailable' | 'system.latestVersion' | 'ui.hasVisitedEasySetup' -export type KVStoreValue = string | null \ No newline at end of file +export const KV_STORE_SCHEMA = { + 'chat.suggestionsEnabled': 'boolean', + 'rag.docsEmbedded': 'boolean', + 'system.updateAvailable': 'boolean', + 'system.latestVersion': 'string', + 'ui.hasVisitedEasySetup': 'boolean', +} as const + +type KVTagToType = T extends 'boolean' ? boolean : string + +export type KVStoreKey = keyof typeof KV_STORE_SCHEMA +export type KVStoreValue = KVTagToType<(typeof KV_STORE_SCHEMA)[K]>