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 { NextRequest } from 'next/server'; |
| #2 | import { authErrorResponse, CANONICAL_KING_ID, CANONICAL_MAAXX_ID, getUserContext, type UserContext } from '@/lib/user-context'; |
| #3 | import { spawn } from 'child_process'; |
| #4 | |
| #5 | export const runtime = 'nodejs'; |
| #6 | export const maxDuration = 3600; |
| #7 | |
| #8 | export async function GET(req: NextRequest) { |
| #9 | let ctx: UserContext; |
| #10 | try { |
| #11 | ctx = await getUserContext(); |
| #12 | } catch (error) { |
| #13 | return authErrorResponse(error); |
| #14 | } |
| #15 | |
| #16 | const stream = new ReadableStream({ |
| #17 | async start(controller) { |
| #18 | const enc = new TextEncoder(); |
| #19 | controller.enqueue(enc.encode(`: connected to ${ctx.displayName} glasses bridge\n\n`)); |
| #20 | |
| #21 | const heartbeat = setInterval(() => { |
| #22 | try { |
| #23 | controller.enqueue(enc.encode(`: heartbeat ${Date.now()}\n\n`)); |
| #24 | } catch {} |
| #25 | }, 15000); |
| #26 | |
| #27 | const tail = spawn('tail', ['-F', '-n', '100', ctx.glassesLogPath]); |
| #28 | let buffer = ''; |
| #29 | |
| #30 | tail.stdout.on('data', (chunk: Buffer) => { |
| #31 | buffer += chunk.toString(); |
| #32 | const lines = buffer.split('\n'); |
| #33 | buffer = lines.pop() ?? ''; |
| #34 | |
| #35 | for (const line of lines) { |
| #36 | if (!line.trim() || !lineBelongsToContext(line, ctx)) continue; |
| #37 | const event = parseLogLine(line); |
| #38 | try { |
| #39 | controller.enqueue(enc.encode(`data: ${JSON.stringify(event)}\n\n`)); |
| #40 | } catch {} |
| #41 | } |
| #42 | }); |
| #43 | |
| #44 | tail.stderr.on('data', (chunk: Buffer) => { |
| #45 | const event = { |
| #46 | ts: new Date().toISOString(), |
| #47 | type: 'stderr', |
| #48 | text: chunk.toString().trim(), |
| #49 | }; |
| #50 | try { |
| #51 | controller.enqueue(enc.encode(`data: ${JSON.stringify(event)}\n\n`)); |
| #52 | } catch {} |
| #53 | }); |
| #54 | |
| #55 | tail.on('close', () => { |
| #56 | clearInterval(heartbeat); |
| #57 | try { controller.close(); } catch {} |
| #58 | }); |
| #59 | tail.on('error', (e) => { |
| #60 | clearInterval(heartbeat); |
| #61 | try { controller.error(e); } catch {} |
| #62 | }); |
| #63 | |
| #64 | req.signal.addEventListener('abort', () => { |
| #65 | clearInterval(heartbeat); |
| #66 | tail.kill(); |
| #67 | try { controller.close(); } catch {} |
| #68 | }); |
| #69 | }, |
| #70 | }); |
| #71 | |
| #72 | return new Response(stream, { |
| #73 | headers: { |
| #74 | 'Content-Type': 'text/event-stream', |
| #75 | 'Cache-Control': 'no-cache, no-transform', |
| #76 | 'Connection': 'keep-alive', |
| #77 | 'X-Accel-Buffering': 'no', |
| #78 | }, |
| #79 | }); |
| #80 | } |
| #81 | |
| #82 | function lineBelongsToContext(line: string, ctx: UserContext) { |
| #83 | const lower = line.toLowerCase(); |
| #84 | if (ctx.canonicalMemberId === CANONICAL_KING_ID) { |
| #85 | return !lower.includes('queenmaaxx') && !lower.includes('maaxx') && !lower.includes(CANONICAL_MAAXX_ID); |
| #86 | } |
| #87 | if (ctx.canonicalMemberId === CANONICAL_MAAXX_ID) { |
| #88 | return !lower.includes('king bau') && !lower.includes(CANONICAL_KING_ID); |
| #89 | } |
| #90 | return true; |
| #91 | } |
| #92 | |
| #93 | function parseLogLine(line: string): any { |
| #94 | const match = line.match(/^\[([A-Z_]+)\]\s*(\{.*\})\s*$/); |
| #95 | if (match) { |
| #96 | try { |
| #97 | return JSON.parse(match[2]); |
| #98 | } catch {} |
| #99 | } |
| #100 | |
| #101 | return { |
| #102 | ts: new Date().toISOString(), |
| #103 | type: 'log', |
| #104 | text: line.slice(0, 500), |
| #105 | }; |
| #106 | } |
| #107 |