feat(F-139): completed feature
This commit is contained in:
@@ -19,13 +19,17 @@ export async function GET(req: NextRequest) {
|
||||
headers: { Cookie: cookies },
|
||||
});
|
||||
if (path === 'admin/logs/stream') {
|
||||
// Pipe SSE directly: Next.js App Router supports ReadableStream passthrough.
|
||||
// X-Accel-Buffering: no tells any intermediate proxy (nginx) not to buffer.
|
||||
const headers = new Headers();
|
||||
headers.set('Content-Type', backendRes.headers.get('content-type') ?? 'text/event-stream');
|
||||
headers.set('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||
headers.set('X-Accel-Buffering', 'no');
|
||||
// Do NOT set Connection: keep-alive — it is HTTP/1.1 default for persistent
|
||||
// connections and can confuse proxies that do not expect streaming.
|
||||
return new Response(backendRes.body, {
|
||||
status: backendRes.status,
|
||||
headers: {
|
||||
'Content-Type': backendRes.headers.get('content-type') ?? 'text/event-stream',
|
||||
'Cache-Control': backendRes.headers.get('cache-control') ?? 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
const data = await backendRes.json().catch(() => null);
|
||||
|
||||
@@ -62,8 +62,12 @@ export function ServerLogViewer({ backendUrl = 'http://192.168.18.93:3000' }: Se
|
||||
// Build the SSE URL with the session cookie forwarded
|
||||
// We use a fetch-based approach: open a GET request and read as text/event-stream
|
||||
let aborted = false;
|
||||
let retryDelay = 1000; // ms — exponential backoff
|
||||
const MAX_DELAY = 30_000;
|
||||
|
||||
const connect = async () => {
|
||||
if (aborted) return;
|
||||
setStatus('connecting');
|
||||
try {
|
||||
// Use the same-origin proxy so the httpOnly backoffice cookie is forwarded server-side.
|
||||
const response = await fetch('/api/admin/logs/stream', {
|
||||
@@ -71,22 +75,30 @@ export function ServerLogViewer({ backendUrl = 'http://192.168.18.93:3000' }: Se
|
||||
});
|
||||
|
||||
if (!response.ok || aborted) {
|
||||
if (!aborted) setStatus('reconnecting');
|
||||
if (!aborted) scheduleRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) { setStatus('error'); return; }
|
||||
if (!reader) { if (!aborted) scheduleRetry(); return; }
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
setStatus('live');
|
||||
retryDelay = 1000; // reset backoff on success
|
||||
|
||||
while (true) {
|
||||
if (aborted) break;
|
||||
if (aborted) {
|
||||
reader.cancel();
|
||||
break;
|
||||
}
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
if (done) {
|
||||
// Stream closed by server — schedule reconnect
|
||||
if (!aborted) scheduleRetry();
|
||||
break;
|
||||
}
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() ?? '';
|
||||
@@ -104,10 +116,19 @@ export function ServerLogViewer({ backendUrl = 'http://192.168.18.93:3000' }: Se
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if (!aborted) setStatus('reconnecting');
|
||||
if (!aborted) scheduleRetry();
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleRetry = () => {
|
||||
if (aborted) return;
|
||||
setStatus('reconnecting');
|
||||
setTimeout(() => {
|
||||
retryDelay = Math.min(retryDelay * 2, MAX_DELAY);
|
||||
connect();
|
||||
}, retryDelay);
|
||||
};
|
||||
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
|
||||
Reference in New Issue
Block a user