Files
obsidian-rag/src/services/health.ts
Santhosh Janardhanan a12e27b83a fix(bridge): resolve data dir same as Python for sync-result.json
Both readSyncResult() and probeAll() now mirror Python's
_resolve_data_dir() logic: dev detection (cwd/.obsidian-rag
or cwd/KnowledgeVault) then home dir fallback.

Previously readSyncResult always used cwd/.obsidian-rag (wrong for
server deployments) and probeAll resolved sync-result.json relative
to db path (wrong for absolute paths like /home/san/.obsidian-rag/).
2026-04-12 00:10:38 -04:00

143 lines
3.9 KiB
TypeScript

/** Health state machine: HEALTHY / DEGRADED / UNAVAILABLE. */
import { existsSync, readFileSync } from "fs";
import { resolve } from "path";
import type { ObsidianRagConfig } from "../utils/config.js";
export type HealthState = "healthy" | "degraded" | "unavailable";
export interface HealthStatus {
state: HealthState;
ollama_up: boolean;
index_exists: boolean;
vault_exists: boolean;
total_docs: number;
total_chunks: number;
last_sync: string | null;
active_job: { id: string; mode: string; progress: number } | null;
}
export interface ProbeResult {
ollama_up: boolean;
index_exists: boolean;
vault_exists: boolean;
total_docs: number;
total_chunks: number;
last_sync: string | null;
}
const REPROBE_INTERVAL_MS = 30_000;
export function createHealthMachine(_config: ObsidianRagConfig) {
let currentState: HealthState = "unavailable";
let status: ProbeResult = {
ollama_up: false,
index_exists: false,
vault_exists: false,
total_docs: 0,
total_chunks: 0,
last_sync: null,
};
let activeJob: { id: string; mode: string; progress: number } | null = null;
let reprobeTimer: ReturnType<typeof setInterval> | null = null;
function transition(probe: ProbeResult): void {
status = probe;
const prev = currentState;
if (!probe.index_exists || !probe.vault_exists) {
currentState = "unavailable";
} else if (!probe.ollama_up) {
currentState = "degraded";
} else {
currentState = "healthy";
}
if (prev !== currentState) {
console.log(`[obsidian-rag] Health: ${prev}${currentState}`);
}
}
function get(): HealthStatus {
return { state: currentState, ...status, active_job: activeJob };
}
function setActiveJob(job: { id: string; mode: string; progress: number } | null): void {
activeJob = job;
}
function startReprobing(fn: () => Promise<ProbeResult>): void {
if (reprobeTimer) clearInterval(reprobeTimer);
reprobeTimer = setInterval(async () => {
const probe = await fn();
transition(probe);
}, REPROBE_INTERVAL_MS);
}
function stop(): void {
if (reprobeTimer) {
clearInterval(reprobeTimer);
reprobeTimer = null;
}
}
return { get, transition, setActiveJob, startReprobing, stop };
}
export async function probeAll(config: ObsidianRagConfig): Promise<ProbeResult> {
const { resolveVectorDbPath } = await import("../utils/lancedb.js");
const vaultPath = resolve(process.cwd(), config.vault_path);
const dbPath = resolveVectorDbPath(config);
const vaultExists = existsSync(vaultPath);
const indexExists = existsSync(String(dbPath));
const ollamaUp = await probeOllama(config.embedding.base_url);
let totalDocs = 0;
let totalChunks = 0;
let lastSync: string | null = null;
if (indexExists) {
try {
const dataDir = resolveDataDir();
const syncPath = resolve(dataDir, "sync-result.json");
if (existsSync(syncPath)) {
const data = JSON.parse(readFileSync(syncPath, "utf-8"));
lastSync = data.timestamp ?? null;
totalDocs = data.indexed_files ?? 0;
totalChunks = data.total_chunks ?? 0;
}
} catch {
// ignore
}
}
return {
ollama_up: ollamaUp,
index_exists: indexExists,
vault_exists: vaultExists,
total_docs: totalDocs,
total_chunks: totalChunks,
last_sync: lastSync,
};
}
function resolveDataDir(): string {
const cwd = process.cwd();
const devDataDir = resolve(cwd, ".obsidian-rag");
const devVaultMarker = resolve(cwd, "KnowledgeVault");
if (existsSync(devDataDir) || existsSync(devVaultMarker)) {
return devDataDir;
}
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
return resolve(home, ".obsidian-rag");
}
async function probeOllama(baseUrl: string): Promise<boolean> {
try {
const res = await fetch(`${baseUrl}/api/tags`, { signal: AbortSignal.timeout(3000) });
return res.ok;
} catch {
return false;
}
}