Add live log streaming via SSE
- Add /logs/stream SSE endpoint that spawns journalctl -f - Stream new log lines to client in real-time - Auto-scroll to bottom unless user has scrolled up - Clean up process when client disconnects Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
98c5d48846
commit
10cb94588d
|
|
@ -33,8 +33,38 @@ export const LogsPage = ({ service, logs }: LogsPageProps) => (
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<pre style="margin-top: 1rem;">
|
<pre id="logs" style="margin-top: 1rem; max-height: 70vh; overflow-y: auto;">
|
||||||
<code>{logs.trim()}</code>
|
<code>{logs.trim()}</code>
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
|
<script dangerouslySetInnerHTML={{ __html: `
|
||||||
|
(function() {
|
||||||
|
const logsEl = document.getElementById('logs');
|
||||||
|
const codeEl = logsEl.querySelector('code');
|
||||||
|
let userScrolledUp = false;
|
||||||
|
|
||||||
|
logsEl.addEventListener('scroll', () => {
|
||||||
|
const atBottom = logsEl.scrollTop + logsEl.clientHeight >= logsEl.scrollHeight - 50;
|
||||||
|
userScrolledUp = !atBottom;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scroll to bottom initially
|
||||||
|
logsEl.scrollTop = logsEl.scrollHeight;
|
||||||
|
|
||||||
|
const service = new URLSearchParams(location.search).get('service') || 'phone-ap';
|
||||||
|
const es = new EventSource('/logs/stream?service=' + encodeURIComponent(service));
|
||||||
|
|
||||||
|
es.onmessage = (e) => {
|
||||||
|
codeEl.textContent += '\\n' + e.data;
|
||||||
|
if (!userScrolledUp) {
|
||||||
|
logsEl.scrollTop = logsEl.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
es.onerror = () => {
|
||||||
|
console.error('SSE connection lost, reconnecting...');
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
`}} />
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
|
import { streamSSE } from "hono/streaming"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
import { IndexPage } from "./components/IndexPage"
|
import { IndexPage } from "./components/IndexPage"
|
||||||
|
|
@ -55,6 +56,36 @@ app.get("/logs", async (c) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// SSE endpoint for live log streaming
|
||||||
|
app.get("/logs/stream", async (c) => {
|
||||||
|
const service = c.req.query("service") || "phone"
|
||||||
|
const validServices = ["phone-ap", "phone-web", "phone"]
|
||||||
|
const selectedService = validServices.includes(service) ? service : "phone"
|
||||||
|
|
||||||
|
return streamSSE(c, async (stream) => {
|
||||||
|
const proc = Bun.spawn(
|
||||||
|
["journalctl", "-u", `${selectedService}.service`, "-f", "-n", "0", "--no-pager", "--no-hostname"],
|
||||||
|
{ stdout: "pipe" }
|
||||||
|
)
|
||||||
|
|
||||||
|
const reader = proc.stdout.getReader()
|
||||||
|
const decoder = new TextDecoder()
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read()
|
||||||
|
if (done) break
|
||||||
|
const text = decoder.decode(value)
|
||||||
|
for (const line of text.split("\n").filter(Boolean)) {
|
||||||
|
await stream.writeSSE({ data: line })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
proc.kill()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Handle WiFi configuration submission
|
// Handle WiFi configuration submission
|
||||||
app.post("/save", async (c) => {
|
app.post("/save", async (c) => {
|
||||||
const formData = await c.req.parseBody()
|
const formData = await c.req.parseBody()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user