|
|
|
|
@ -14,7 +14,7 @@
|
|
|
|
|
<option value="1000" selected>1000</option>
|
|
|
|
|
<option value="5000">5000</option>
|
|
|
|
|
<option value="10000">10000</option>
|
|
|
|
|
<option value="-1">전체</option>
|
|
|
|
|
<!-- <option value="-1">전체</option>-->
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
@ -36,66 +36,59 @@
|
|
|
|
|
let running = false;
|
|
|
|
|
let lineQueue = [];
|
|
|
|
|
let maxLines = 3000;
|
|
|
|
|
let cursor = null; // 🔑 서버에서 받은 X-Log-Position 저장
|
|
|
|
|
|
|
|
|
|
function appendOneLine(line) {
|
|
|
|
|
const div = document.createElement("div");
|
|
|
|
|
div.textContent = line;
|
|
|
|
|
div.style.opacity = "0";
|
|
|
|
|
div.style.transform = "translateY(2px)";
|
|
|
|
|
div.style.transition = "opacity 0.15s ease, transform 0.15s ease";
|
|
|
|
|
container.appendChild(div);
|
|
|
|
|
while (container.childNodes.length > maxLines) container.removeChild(container.firstChild);
|
|
|
|
|
requestAnimationFrame(() => { div.style.opacity = "1"; div.style.transform = "translateY(0)"; });
|
|
|
|
|
const nearBottom = (container.scrollHeight - container.scrollTop - container.clientHeight) < 40;
|
|
|
|
|
if (nearBottom) container.scrollTop = container.scrollHeight;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function initLogs() {
|
|
|
|
|
const selected = lineSel.value;
|
|
|
|
|
const param = selected === "-1" ? "" : `?lines=${selected}`;
|
|
|
|
|
container.innerHTML = ""; // 초기화
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/logs/init${param}`, { cache: "no-store" });
|
|
|
|
|
if (!res.ok) return;
|
|
|
|
|
const text = await res.text();
|
|
|
|
|
if (!text) return;
|
|
|
|
|
|
|
|
|
|
const lines = text.split("\n").filter(l => l.length > 0);
|
|
|
|
|
lines.forEach(appendOneLine);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.debug("initLogs error:", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
container.innerHTML = "";
|
|
|
|
|
|
|
|
|
|
async function fetchLogs() {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch("/logs/stream", { cache: "no-store" });
|
|
|
|
|
if (!res.ok) return;
|
|
|
|
|
const text = await res.text();
|
|
|
|
|
if (!text) return;
|
|
|
|
|
const res = await fetch(`/logs/init${param}`, { cache: "no-store" });
|
|
|
|
|
const text = await res.text();
|
|
|
|
|
|
|
|
|
|
const lines = text.split("\n").filter(l => l.length > 0);
|
|
|
|
|
lineQueue.push(...lines);
|
|
|
|
|
// 🔑 초기 포인터 저장
|
|
|
|
|
const pos = res.headers.get("X-Log-Position");
|
|
|
|
|
if (pos) cursor = Number(pos);
|
|
|
|
|
|
|
|
|
|
if (lineQueue.length > 100_000) lineQueue = lineQueue.slice(-50_000);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.debug("fetchLogs error:", e);
|
|
|
|
|
}
|
|
|
|
|
if (text.trim()) text.split("\n").forEach(l => l && appendOneLine(l));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function appendOneLine(line) {
|
|
|
|
|
const div = document.createElement("div");
|
|
|
|
|
div.textContent = line;
|
|
|
|
|
div.style.opacity = "0";
|
|
|
|
|
div.style.transform = "translateY(2px)";
|
|
|
|
|
div.style.transition = "opacity 0.15s ease, transform 0.15s ease";
|
|
|
|
|
container.appendChild(div);
|
|
|
|
|
async function fetchLogs() {
|
|
|
|
|
// 🔑 클라이언트가 cursor를 들고 있다가 파라미터로 전달
|
|
|
|
|
const url = (cursor == null) ? "/logs/stream" : `/logs/stream?from=${cursor}`;
|
|
|
|
|
const res = await fetch(url, { cache: "no-store" });
|
|
|
|
|
const text = await res.text();
|
|
|
|
|
|
|
|
|
|
while (container.childNodes.length > maxLines) {
|
|
|
|
|
container.removeChild(container.firstChild);
|
|
|
|
|
}
|
|
|
|
|
// 🔑 새 포인터로 갱신
|
|
|
|
|
const pos = res.headers.get("X-Log-Position");
|
|
|
|
|
if (pos) cursor = Number(pos);
|
|
|
|
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
div.style.opacity = "1";
|
|
|
|
|
div.style.transform = "translateY(0)";
|
|
|
|
|
});
|
|
|
|
|
// 파일 리셋 감지(선택): 필요시 재-init
|
|
|
|
|
// if (res.headers.get("X-Log-Reset") === "true") { await initLogs(); return; }
|
|
|
|
|
|
|
|
|
|
const nearBottom = (container.scrollHeight - container.scrollTop - container.clientHeight) < 40;
|
|
|
|
|
if (nearBottom) container.scrollTop = container.scrollHeight;
|
|
|
|
|
if (text.trim()) {
|
|
|
|
|
const lines = text.split("\n").filter(Boolean);
|
|
|
|
|
lineQueue.push(...lines);
|
|
|
|
|
if (lineQueue.length > 100_000) lineQueue = lineQueue.slice(-50_000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function startRenderLoop() {
|
|
|
|
|
const baseMs = Number(speedSel.value) || 25;
|
|
|
|
|
if (renderTimer) clearInterval(renderTimer);
|
|
|
|
|
|
|
|
|
|
renderTimer = setInterval(() => {
|
|
|
|
|
if (!running) return;
|
|
|
|
|
const burst = Math.min(5, Math.max(1, Math.floor(lineQueue.length / 200)));
|
|
|
|
|
@ -113,8 +106,7 @@
|
|
|
|
|
startBtn.disabled = true;
|
|
|
|
|
stopBtn.disabled = false;
|
|
|
|
|
|
|
|
|
|
await initLogs(); // ✅ 선택된 라인 수만큼 초기 로드
|
|
|
|
|
|
|
|
|
|
await initLogs(); // 최근 N줄 + cursor 수신
|
|
|
|
|
pollingTimer = setInterval(fetchLogs, 2000);
|
|
|
|
|
fetchLogs();
|
|
|
|
|
startRenderLoop();
|
|
|
|
|
@ -128,10 +120,7 @@
|
|
|
|
|
if (renderTimer) { clearInterval(renderTimer); renderTimer = null; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
speedSel.addEventListener("change", () => {
|
|
|
|
|
if (running) startRenderLoop();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
speedSel.addEventListener("change", () => { if (running) startRenderLoop(); });
|
|
|
|
|
startBtn.addEventListener("click", start);
|
|
|
|
|
stopBtn.addEventListener("click", stop);
|
|
|
|
|
window.addEventListener("beforeunload", stop);
|
|
|
|
|
|