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/).
143 lines
3.9 KiB
TypeScript
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;
|
|
}
|
|
}
|