repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
The Living OS cockpit
stars
latest
clone command
git clone gitlawb://did:key:z6Mku78K...XywC/living-os-cockp...git clone gitlawb://did:key:z6Mku78K.../living-os-cockp...59751530feat: surface worker supervisor health in live work8h ago| #1 | import { NextResponse } from 'next/server'; |
| #2 | import { promises as fs } from 'fs'; |
| #3 | import { authErrorResponse, getUserContext } from '@/lib/user-context'; |
| #4 | import { getServices } from '@/lib/services'; |
| #5 | import { execFile as execFileCb } from 'child_process'; |
| #6 | import { promisify } from 'util'; |
| #7 | |
| #8 | export const runtime = 'nodejs'; |
| #9 | export const dynamic = 'force-dynamic'; |
| #10 | |
| #11 | const execFile = promisify(execFileCb); |
| #12 | const OS_EVENTS_PATH = process.env.THELIVING_EVENT_SINK ?? '/home/kingbau/logs/theliving-os/events.jsonl'; |
| #13 | const BOOK_AUDIT_REPORT = process.env.BOOK_SOURCE_AUDIT_REPORT ?? '/home/kingbau/Documents/Aethon-Core/data/book_source_audit/ongoing/BOOK_SOURCE_AUDIT.md'; |
| #14 | const BOOK_AUDIT_EVENTS = process.env.BOOK_SOURCE_AUDIT_EVENTS ?? '/home/kingbau/Documents/Aethon-Core/data/book_source_audit/ongoing/events.jsonl'; |
| #15 | const BOOK_AUDIT_LOG = process.env.BOOK_SOURCE_AUDIT_LOG ?? '/home/kingbau/Documents/Aethon-Core/data/book_source_audit/ongoing/nohup.log'; |
| #16 | const BOOK_AUDIT_LOCK = process.env.BOOK_SOURCE_AUDIT_LOCK ?? '/home/kingbau/Documents/Aethon-Core/data/book_source_audit/ongoing/book_source_audit_ingest.lock'; |
| #17 | const CATALOG_HUNTER_REPORT = process.env.CATALOG_HUNTER_REPORT ?? '/home/kingbau/Documents/Aethon-Core/data/catalog_hunter/ongoing/CATALOG_HUNTER_REPORT.md'; |
| #18 | const CATALOG_HUNTER_PROGRESS = process.env.CATALOG_HUNTER_PROGRESS ?? '/home/kingbau/Documents/Aethon-Core/data/catalog_hunter/ongoing/progress.json'; |
| #19 | const CATALOG_HUNTER_EVENTS = process.env.CATALOG_HUNTER_EVENTS ?? '/home/kingbau/Documents/Aethon-Core/data/catalog_hunter/ongoing/events.jsonl'; |
| #20 | const CATALOG_HUNTER_LOG = process.env.CATALOG_HUNTER_LOG ?? '/home/kingbau/Documents/Aethon-Core/data/catalog_hunter/ongoing/nohup.log'; |
| #21 | |
| #22 | type ServiceResult = { |
| #23 | id: string; |
| #24 | label: string; |
| #25 | category: string; |
| #26 | status: 'up' | 'down' | 'watching'; |
| #27 | code?: number | string; |
| #28 | error?: string; |
| #29 | detail?: string; |
| #30 | }; |
| #31 | |
| #32 | export async function GET() { |
| #33 | try { |
| #34 | const ctx = await getUserContext(); |
| #35 | const services = getServices(ctx); |
| #36 | |
| #37 | const results = await Promise.all( |
| #38 | services.map((svc) => checkHttpService(svc.id, svc.label, svc.url, svc.category, ctx.legacyUserId)) |
| #39 | ); |
| #40 | const [opsServices, runningNow, recentEvents] = await Promise.all([ |
| #41 | namedOpsServices(), |
| #42 | runningWork(), |
| #43 | recentOpsEvents(), |
| #44 | ]); |
| #45 | |
| #46 | return NextResponse.json({ |
| #47 | user: ctx.userId, |
| #48 | legacyUser: ctx.legacyUserId, |
| #49 | displayName: ctx.displayName, |
| #50 | services: mergeServices([...results, ...opsServices]), |
| #51 | runningNow, |
| #52 | recentEvents, |
| #53 | logSources: [OS_EVENTS_PATH, BOOK_AUDIT_EVENTS, BOOK_AUDIT_LOG, CATALOG_HUNTER_EVENTS, CATALOG_HUNTER_PROGRESS], |
| #54 | ts: Date.now(), |
| #55 | }); |
| #56 | } catch (error) { |
| #57 | return authErrorResponse(error); |
| #58 | } |
| #59 | } |
| #60 | |
| #61 | async function checkHttpService(id: string, label: string, url: string, category: string, legacyUserId = 'master'): Promise<ServiceResult> { |
| #62 | try { |
| #63 | const ctrl = new AbortController(); |
| #64 | const tid = setTimeout(() => ctrl.abort(), 2500); |
| #65 | const r = await fetch(url, { signal: ctrl.signal, cache: 'no-store' }); |
| #66 | clearTimeout(tid); |
| #67 | if (!r.ok) { |
| #68 | const fallback = await systemdFallback(id, legacyUserId); |
| #69 | if (fallback) return { id, label, category, status: 'up', code: fallback, detail: `${url} returned ${r.status}; systemd is active` }; |
| #70 | } |
| #71 | return { id, label, category, status: r.ok ? 'up' : 'down', code: r.status, detail: url }; |
| #72 | } catch (e: unknown) { |
| #73 | const fallback = await systemdFallback(id, legacyUserId); |
| #74 | if (fallback) return { id, label, category, status: 'up', code: fallback, detail: `${url} did not answer; systemd is active` }; |
| #75 | const message = e instanceof Error ? e.message : 'unknown'; |
| #76 | return { id, label, category, status: 'down', error: message.slice(0, 140), detail: url }; |
| #77 | } |
| #78 | } |
| #79 | |
| #80 | async function systemdFallback(serviceId: string, userId: string): Promise<string | null> { |
| #81 | const service = systemdServiceFor(serviceId, userId); |
| #82 | if (!service) return null; |
| #83 | |
| #84 | for (const args of [ |
| #85 | ['--user', 'is-active', service], |
| #86 | ['is-active', service], |
| #87 | ]) { |
| #88 | try { |
| #89 | const { stdout } = await execFile('systemctl', args, { timeout: 2000 }); |
| #90 | if (stdout.trim() === 'active') return args[0] === '--user' ? 'systemd:user' : 'systemd'; |
| #91 | } catch {} |
| #92 | } |
| #93 | return null; |
| #94 | } |
| #95 | |
| #96 | function systemdServiceFor(serviceId: string, userId: string) { |
| #97 | return serviceId === 'aethon' || serviceId === 'daemon-context' |
| #98 | ? userId === 'queen_maaxx' |
| #99 | ? 'aethon-daemon-maaxx.service' |
| #100 | : 'aethon-daemon.service' |
| #101 | : serviceId === 'glasses' || serviceId === 'bridge-context' |
| #102 | ? userId === 'queen_maaxx' |
| #103 | ? 'aethon-glasses-maaxx.service' |
| #104 | : 'aethon-glasses-king.service' |
| #105 | : serviceId === 'daemon-king' |
| #106 | ? 'aethon-daemon.service' |
| #107 | : serviceId === 'daemon-maaxx' |
| #108 | ? 'aethon-daemon-maaxx.service' |
| #109 | : serviceId === 'bridge-king' |
| #110 | ? 'aethon-glasses-king.service' |
| #111 | : serviceId === 'bridge-maaxx' |
| #112 | ? 'aethon-glasses-maaxx.service' |
| #113 | : serviceId === 'cockpit' |
| #114 | ? 'living-os-cockpit.service' |
| #115 | : ''; |
| #116 | } |
| #117 | |
| #118 | async function namedOpsServices(): Promise<ServiceResult[]> { |
| #119 | const manual = await Promise.all([ |
| #120 | checkHttpService('cockpit', 'Cockpit UI (:4000)', 'http://127.0.0.1:4000/health', 'core'), |
| #121 | checkHttpService('daemon-king', 'Daemon King (:7000)', 'http://127.0.0.1:7000/health', 'agent', 'master'), |
| #122 | checkHttpService('daemon-maaxx', 'Daemon Maaxx (:7001)', 'http://127.0.0.1:7001/health', 'agent', 'queen_maaxx'), |
| #123 | checkHttpService('bridge-king', 'Glasses Bridge King (:3100)', 'http://127.0.0.1:3100/health', 'glasses', 'master'), |
| #124 | checkHttpService('bridge-maaxx', 'Glasses Bridge Maaxx (:3101)', 'http://127.0.0.1:3101/health', 'glasses', 'queen_maaxx'), |
| #125 | checkHttpService('embedder-cpu', 'Qwen3 Embedder CPU sidecar (:8002)', 'http://127.0.0.1:8002/health', 'inference'), |
| #126 | checkHttpService('embedder-gpu', 'Qwen3 GPU Embedder (:8001 bulk only)', 'http://127.0.0.1:8001/health', 'inference'), |
| #127 | checkHttpService('openyourmind', 'OpenYourMind Local (:8003)', 'http://127.0.0.1:8003/health', 'inference'), |
| #128 | ]); |
| #129 | const [ingest, turbovec] = await Promise.all([ingestWorkerService(), turbovecService()]); |
| #130 | return [...manual, ingest, turbovec]; |
| #131 | } |
| #132 | |
| #133 | async function ingestWorkerService(): Promise<ServiceResult> { |
| #134 | const progress = await bookAuditProgress(); |
| #135 | try { |
| #136 | const { stdout } = await execFile('pgrep', ['-af', 'book_source_audit_ingest.py'], { timeout: 2000 }); |
| #137 | const active = stdout.trim().length > 0; |
| #138 | return { |
| #139 | id: 'ingest-worker', |
| #140 | label: 'Book/source ingest worker', |
| #141 | category: 'core', |
| #142 | status: active ? 'up' : 'down', |
| #143 | code: active ? 'process' : 'pgrep', |
| #144 | detail: progress?.summary ?? (active ? 'running' : 'not running'), |
| #145 | }; |
| #146 | } catch { |
| #147 | return { |
| #148 | id: 'ingest-worker', |
| #149 | label: 'Book/source ingest worker', |
| #150 | category: 'core', |
| #151 | status: 'down', |
| #152 | error: 'process not found', |
| #153 | detail: progress?.summary ?? BOOK_AUDIT_LOCK, |
| #154 | }; |
| #155 | } |
| #156 | } |
| #157 | |
| #158 | async function turbovecService(): Promise<ServiceResult> { |
| #159 | const paths = [ |
| #160 | '/home/kingbau/vaults/sovereign/turbovec', |
| #161 | '/home/kingbau/vaults/kingbau/turbovec', |
| #162 | '/home/kingbau/Documents/Aethon-Core/data/turbovec', |
| #163 | ]; |
| #164 | for (const path of paths) { |
| #165 | try { |
| #166 | const stat = await fs.stat(path); |
| #167 | if (stat.isDirectory()) return { id: 'turbovec', label: 'Turbovec vault store', category: 'core', status: 'up', code: 'path', detail: path }; |
| #168 | } catch {} |
| #169 | } |
| #170 | return { id: 'turbovec', label: 'Turbovec vault store', category: 'core', status: 'down', error: 'store path not found', detail: paths.join(' | ') }; |
| #171 | } |
| #172 | |
| #173 | function mergeServices(services: ServiceResult[]) { |
| #174 | const seen = new Set<string>(); |
| #175 | return services.filter((service) => { |
| #176 | if (seen.has(service.id)) return false; |
| #177 | seen.add(service.id); |
| #178 | return true; |
| #179 | }); |
| #180 | } |
| #181 | |
| #182 | async function bookAuditProgress() { |
| #183 | try { |
| #184 | const report = await fs.readFile(BOOK_AUDIT_REPORT, 'utf-8'); |
| #185 | const total = Number(report.match(/Total on-disk documents audited:\s*`?(\d+)/i)?.[1] ?? 0); |
| #186 | const embedded = Number(report.match(/Embedded documents matched:\s*`?(\d+)/i)?.[1] ?? 0); |
| #187 | const missing = Number(report.match(/Missing documents to ingest:\s*`?(\d+)/i)?.[1] ?? 0); |
| #188 | const generated = report.match(/Generated:\s*`([^`]+)`/i)?.[1] ?? ''; |
| #189 | const percent = total ? Math.round((embedded / total) * 1000) / 10 : 0; |
| #190 | return { |
| #191 | total, |
| #192 | embedded, |
| #193 | missing, |
| #194 | generated, |
| #195 | percent, |
| #196 | summary: `Book ingest ${embedded}/${total} embedded (${percent}%), ${missing} missing.`, |
| #197 | }; |
| #198 | } catch { |
| #199 | return null; |
| #200 | } |
| #201 | } |
| #202 | |
| #203 | async function runningWork() { |
| #204 | const [progress, catalog] = await Promise.all([bookAuditProgress(), catalogHunterProgress()]); |
| #205 | const rows = []; |
| #206 | if (progress) { |
| #207 | rows.push({ |
| #208 | id: 'book-source-audit', |
| #209 | label: 'Book/source ingest', |
| #210 | status: progress.missing > 0 ? 'running' : 'complete', |
| #211 | message: progress.summary, |
| #212 | progress: progress.percent, |
| #213 | source: BOOK_AUDIT_REPORT, |
| #214 | updated_at: progress.generated, |
| #215 | }); |
| #216 | } |
| #217 | if (catalog) { |
| #218 | rows.push({ |
| #219 | id: 'catalog-hunter', |
| #220 | label: 'Catalog hunter', |
| #221 | status: catalog.status, |
| #222 | message: catalog.summary, |
| #223 | progress: catalog.progress, |
| #224 | source: CATALOG_HUNTER_PROGRESS, |
| #225 | updated_at: catalog.updated_at, |
| #226 | detail: catalog, |
| #227 | }); |
| #228 | } |
| #229 | const latest = await latestJsonLine(BOOK_AUDIT_EVENTS); |
| #230 | if (latest) { |
| #231 | rows.push({ |
| #232 | id: `book-source-event-${latest.at ?? latest.timestamp ?? 'latest'}`, |
| #233 | label: 'Latest ingest event', |
| #234 | status: String(latest.event ?? 'event'), |
| #235 | message: summarizeEvent(latest), |
| #236 | source: BOOK_AUDIT_EVENTS, |
| #237 | updated_at: latest.at ?? latest.timestamp ?? '', |
| #238 | }); |
| #239 | } |
| #240 | const latestCatalog = await latestJsonLine(CATALOG_HUNTER_EVENTS); |
| #241 | if (latestCatalog) { |
| #242 | rows.push({ |
| #243 | id: `catalog-hunter-event-${latestCatalog.at ?? latestCatalog.timestamp ?? 'latest'}`, |
| #244 | label: 'Latest catalog event', |
| #245 | status: String(latestCatalog.event ?? latestCatalog.status ?? 'event'), |
| #246 | message: summarizeEvent(latestCatalog), |
| #247 | source: CATALOG_HUNTER_EVENTS, |
| #248 | updated_at: latestCatalog.at ?? latestCatalog.timestamp ?? '', |
| #249 | detail: latestCatalog, |
| #250 | }); |
| #251 | } |
| #252 | return rows; |
| #253 | } |
| #254 | |
| #255 | async function recentOpsEvents() { |
| #256 | const eventSets = await Promise.all([ |
| #257 | tailJsonEvents(OS_EVENTS_PATH, 50), |
| #258 | tailJsonEvents(BOOK_AUDIT_EVENTS, 40), |
| #259 | tailJsonEvents(CATALOG_HUNTER_EVENTS, 40), |
| #260 | ]); |
| #261 | return eventSets |
| #262 | .flat() |
| #263 | .sort((a, b) => Date.parse(String(b.ts || b.at || b.timestamp || '')) - Date.parse(String(a.ts || a.at || a.timestamp || ''))) |
| #264 | .slice(0, 24) |
| #265 | .map((event, index) => ({ |
| #266 | id: `${event.service ?? event.event ?? 'event'}-${event.ts ?? event.at ?? event.timestamp ?? index}-${index}`, |
| #267 | source: event.__source, |
| #268 | label: eventLabel(event), |
| #269 | message: summarizeEvent(event), |
| #270 | ts: event.ts ?? event.at ?? event.timestamp ?? new Date().toISOString(), |
| #271 | event_type: event.event_type ?? event.event ?? 'event', |
| #272 | status: event.status ?? event.event ?? event.event_type ?? 'event', |
| #273 | detail: eventDetail(event), |
| #274 | })); |
| #275 | } |
| #276 | |
| #277 | async function readJsonFile(path: string) { |
| #278 | try { |
| #279 | return JSON.parse(await fs.readFile(path, 'utf-8')); |
| #280 | } catch { |
| #281 | return null; |
| #282 | } |
| #283 | } |
| #284 | |
| #285 | async function catalogHunterProgress() { |
| #286 | const progress = await readJsonFile(CATALOG_HUNTER_PROGRESS); |
| #287 | if (!progress) return null; |
| #288 | const acquire = progress.acquire_summary ?? progress.acquire ?? {}; |
| #289 | const embed = progress.embed_summary ?? progress.embed ?? {}; |
| #290 | const target = progress.current_target ? ` · target ${progress.current_target}` : ''; |
| #291 | const embedded = Number(embed.embedded ?? 0); |
| #292 | const chunks = Number(embed.chunks_added ?? 0); |
| #293 | const acquired = Number(acquire.acquired ?? 0); |
| #294 | const failures = Number(acquire.failures ?? 0) + Number(embed.failed ?? 0); |
| #295 | return { |
| #296 | ...progress, |
| #297 | status: progress.status ?? 'watching', |
| #298 | progress: Number(progress.current_index ?? 0), |
| #299 | summary: `Catalog hunter ${progress.status ?? 'watching'}${target}: acquired ${acquired}, embedded ${embedded}, chunks ${chunks}, failures ${failures}.`, |
| #300 | report: CATALOG_HUNTER_REPORT, |
| #301 | events: CATALOG_HUNTER_EVENTS, |
| #302 | }; |
| #303 | } |
| #304 | |
| #305 | async function tailJsonEvents(path: string, limit = 40) { |
| #306 | try { |
| #307 | const stat = await fs.stat(path); |
| #308 | const handle = await fs.open(path, 'r'); |
| #309 | try { |
| #310 | const readLength = Math.min(stat.size, 240_000); |
| #311 | const buffer = Buffer.alloc(readLength); |
| #312 | await handle.read(buffer, 0, readLength, stat.size - readLength); |
| #313 | const lines = buffer.toString('utf-8').split(/\r?\n/).filter(Boolean).slice(-limit); |
| #314 | return lines.flatMap((line) => { |
| #315 | try { |
| #316 | return [{ ...JSON.parse(line), __source: path }]; |
| #317 | } catch { |
| #318 | return []; |
| #319 | } |
| #320 | }); |
| #321 | } finally { |
| #322 | await handle.close(); |
| #323 | } |
| #324 | } catch { |
| #325 | return []; |
| #326 | } |
| #327 | } |
| #328 | |
| #329 | async function latestJsonLine(path: string) { |
| #330 | const events = await tailJsonEvents(path, 1); |
| #331 | return events[0] ?? null; |
| #332 | } |
| #333 | |
| #334 | function eventLabel(event: Record<string, unknown>) { |
| #335 | const service = String(event.service ?? ''); |
| #336 | if (service === 'bridge_king') return 'King glasses'; |
| #337 | if (service === 'bridge_maaxx') return 'Maaxx glasses'; |
| #338 | if (String(event.__source ?? '').includes('book_source_audit')) return 'Book ingest'; |
| #339 | if (String(event.__source ?? '').includes('catalog_hunter')) return 'Catalog hunter'; |
| #340 | return String(event.service ?? event.event ?? 'Aethon event').replace(/_/g, ' '); |
| #341 | } |
| #342 | |
| #343 | function summarizeEvent(event: Record<string, any>) { |
| #344 | const payload = event.payload || {}; |
| #345 | const type = String(event.event_type ?? event.event ?? 'event'); |
| #346 | if (type === 'transcription') return `Heard: "${String(payload.text ?? '').slice(0, 140)}"`; |
| #347 | if (type === 'wake') return `Wake matched: "${String(payload.trigger ?? '').slice(0, 140)}"`; |
| #348 | if (type === 'wake_miss') return `Wake miss: "${String(payload.text ?? '').slice(0, 140)}"`; |
| #349 | if (type === 'query') return `Query forwarded: "${String(payload.text ?? '').slice(0, 140)}"`; |
| #350 | if (type === 'response') return `Response returned: "${String(payload.text ?? '').slice(0, 140)}"`; |
| #351 | if (type === 'session') return `Session ${payload.event ?? 'updated'}${payload.activeSessions !== undefined ? ` · active ${payload.activeSessions}` : ''}.`; |
| #352 | if (type === 'audit_complete') return `Audit complete: ${event.missing_documents ?? '?'} missing of ${event.total_documents ?? '?'} docs.`; |
| #353 | if (type === 'ingest_pass_complete') return `Ingest pass: ${event.embedded ?? 0} embedded, ${event.failed ?? 0} failed, ${event.pending_before ?? '?'} pending before.`; |
| #354 | if (type === 'acquired') return `Acquired ${event.title ?? event.target_key ?? 'catalog target'}${event.local_path ? ` -> ${event.local_path}` : ''}.`; |
| #355 | if (type === 'embedded') return `Embedded ${event.title ?? event.target_key ?? 'catalog target'}: ${event.chunks_added ?? 0} chunks.`; |
| #356 | if (type === 'needs_ocr') return `Needs OCR: ${event.title ?? event.target_key ?? 'catalog target'} (${event.reason ?? 'no text layer'}).`; |
| #357 | if (type === 'failed_acquire' || type === 'failed_embed') return `${type.replace(/_/g, ' ')}: ${event.target_key ?? event.title ?? 'catalog target'} — ${event.error ?? 'unknown error'}.`; |
| #358 | if (type === 'discovered') return `Discovered ${event.candidate_count ?? 0} candidates for ${event.title ?? event.target_key ?? 'catalog target'}.`; |
| #359 | if (type === 'candidate_rejected_volume_mismatch') return `Rejected volume-mismatch candidate for ${event.title ?? event.target_key ?? 'catalog target'}.`; |
| #360 | if (event.message) return String(event.message).slice(0, 180); |
| #361 | return type.replace(/_/g, ' '); |
| #362 | } |
| #363 | |
| #364 | function eventDetail(event: Record<string, any>) { |
| #365 | const payload = event.payload || {}; |
| #366 | return { |
| #367 | source: event.__source, |
| #368 | service: event.service ?? null, |
| #369 | event_type: event.event_type ?? event.event ?? 'event', |
| #370 | status: event.status ?? null, |
| #371 | timestamp: event.ts ?? event.at ?? event.timestamp ?? null, |
| #372 | user_id: event.user_id ?? event.userId ?? payload.user_id ?? payload.userId ?? null, |
| #373 | daemon: payload.daemon ?? event.daemon ?? null, |
| #374 | vault: payload.vault ?? event.vault ?? null, |
| #375 | transcription: payload.text ?? event.text ?? null, |
| #376 | query: payload.query ?? payload.question ?? null, |
| #377 | response: payload.response ?? payload.answer ?? null, |
| #378 | error: payload.error ?? event.error ?? event.message ?? null, |
| #379 | code: payload.code ?? event.code ?? event.status_code ?? event.returncode ?? null, |
| #380 | stack: payload.stack ?? event.stack ?? null, |
| #381 | payload, |
| #382 | raw: event, |
| #383 | }; |
| #384 | } |
| #385 |