You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

276 lines
14 KiB

(function () {
"use strict";
function transitionsOnly(events) { return events.filter(function (event) { return event.event_type === "transition"; }); }
function orderedEvents(events) {
return events.slice().sort(function (a, b) {
const left = Date.parse(a.timestamp);
const right = Date.parse(b.timestamp);
return (Number.isFinite(right) ? right : -Infinity) - (Number.isFinite(left) ? left : -Infinity);
});
}
function summaryFor(kind, events) {
const groups = kind === "system"
? [["info", "정보"], ["warning", "경고"], ["critical", "치명적"]]
: kind === "port" ? [["rx", "수신 RX"], ["tx", "전송 TX"], ["session", "세션"]]
: [["up", "UP"], ["down", "DOWN"], ["unknown", "미확인"]];
return groups.map(function (group) {
const count = events.filter(function (event) {
if (kind === "system") { return String(event.severity || "").toLowerCase() === group[0]; }
if (kind === "link") { return String(event.state || "unknown").toLowerCase() === group[0]; }
return String(group[0] === "session" ? event.event_type : event.direction).toLowerCase() === group[0];
}).length;
return { key: group[0], label: group[1], count: count };
});
}
function timestampParts(input, timeZone) {
const date = input ? new Date(input) : new Date(NaN);
if (Number.isNaN(date.getTime())) { return { date: "—", time: String(input || "—"), unset: false }; }
try {
const values = {};
new Intl.DateTimeFormat("en-CA", {
year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit",
hourCycle: "h23", timeZone: timeZone
}).formatToParts(date).forEach(function (part) { values[part.type] = part.value; });
return { date: values.year + "-" + values.month + "-" + values.day, time: values.hour + ":" + values.minute + ":" + values.second, unset: date.getUTCFullYear() < 2000 };
} catch (_) {
return { date: "—", time: String(input), unset: date.getUTCFullYear() < 2000 };
}
}
function highlightParts(input, keyword) {
const content = String(input ?? "—");
const needle = String(keyword || "").trim();
if (!needle) { return [{ text: content, match: false }]; }
const lower = content.toLowerCase();
const parts = [];
let offset = 0;
let found;
while ((found = lower.indexOf(needle.toLowerCase(), offset)) !== -1) {
if (found > offset) { parts.push({ text: content.slice(offset, found), match: false }); }
parts.push({ text: content.slice(found, found + needle.length), match: true });
offset = found + needle.length;
}
if (offset < content.length) { parts.push({ text: content.slice(offset), match: false }); }
return parts;
}
const api = { transitionsOnly: transitionsOnly, orderedEvents: orderedEvents, summaryFor: summaryFor, timestampParts: timestampParts, highlightParts: highlightParts };
if (typeof module !== "undefined" && module.exports) { module.exports = api; }
if (typeof window === "undefined" || typeof document === "undefined") { return; }
function node(tag, className, text) {
const element = document.createElement(tag);
if (className) { element.className = className; }
if (text !== undefined) { element.textContent = String(text); }
return element;
}
function raw(element) { element.setAttribute("data-i18n-skip", ""); return element; }
function highlighted(element, content, keyword) {
highlightParts(content, keyword).forEach(function (part) {
element.append(part.match ? node("mark", "", part.text) : document.createTextNode(part.text));
});
}
function badge(label, tone) {
const element = node("span", "log-badge", label);
element.dataset.tone = tone;
return element;
}
function mount(kind, form, panel) {
const filterPanel = form.closest(".panel");
filterPanel.classList.add("log-filter-panel");
panel.classList.add("log-result-panel");
panel.dataset.logKind = kind;
const table = panel.querySelector("table");
table.setAttribute("aria-label", panel.querySelector("h2").textContent);
table.querySelectorAll("th").forEach(function (cell) { cell.scope = "col"; });
const tableBody = table.querySelector("tbody");
const mainGrid = form.querySelector(".form-grid");
const actions = form.querySelector(".panel-actions");
const line = node("div", "log-filter-line");
mainGrid.before(line);
line.append(mainGrid, actions);
const footer = node("div", "log-filter-footer");
const advanced = node("details", "log-advanced");
advanced.appendChild(node("summary", "", "상세 검색 · 기간 / 추가 조건"));
const advancedGrid = node("div", "log-advanced-grid");
["from", "to", "actor"].forEach(function (name) {
const row = form.querySelector('[name="' + name + '"]')?.closest(".form-row");
if (row) { advancedGrid.appendChild(row); }
});
advanced.append(advancedGrid, node("p", "log-search-note", "기간 입력은 브라우저 시간대 기준입니다."));
const notes = form.querySelector(".note-list");
if (notes) { advanced.appendChild(notes); }
footer.appendChild(advanced);
const autoRow = form.querySelector('[name="auto_refresh"]')?.closest(".form-row");
if (autoRow) { autoRow.classList.add("log-auto-refresh"); footer.appendChild(autoRow); }
form.appendChild(footer);
form.querySelectorAll(".form-row").forEach(function (row) {
const input = row.querySelector("input, select");
const label = row.querySelector(":scope > label");
if (!input) { return; }
input.id = "log-" + kind + "-" + input.name;
if (label) { label.htmlFor = input.id; }
if (input.name === "keyword") { row.classList.add("log-keyword-field"); }
});
const overview = node("section", "log-overview");
overview.setAttribute("aria-label", "현재 조회 결과 요약");
const total = node("div", "log-overview-total");
const totalNumber = raw(node("strong", "", "—"));
total.append(node("span", "", "조회 결과"), totalNumber, node("span", "", "건"));
const scope = node("span", "log-overview-scope", "현재 조회된 로그 기준");
const breakdown = node("div", "log-overview-breakdown");
overview.append(total, breakdown, scope);
filterPanel.before(overview);
const feedback = node("p", "log-result-feedback");
feedback.hidden = true;
feedback.setAttribute("role", "status");
panel.querySelector(".table-wrap").before(feedback);
const bottom = node("div", "log-result-footer");
const rangeNote = node("span", "", "현재 검색 조건에 맞는 최신 로그를 표시합니다.");
const updated = node("span", "log-updated");
updated.append(node("span", "", "마지막 조회"), raw(node("time", "", "—")));
bottom.append(rangeNote, updated);
panel.appendChild(bottom);
let previousRows = new Map();
let previousSummary = "";
let hasResult = false;
function makeRow(event, values, timezone, keyword) {
const row = node("tr", "log-event-row");
const dateParts = timestampParts(event.timestamp, timezone);
const dateCell = node("td", "log-time-cell");
const time = raw(node("time", "", dateParts.time));
if (!Number.isNaN(Date.parse(event.timestamp))) { time.dateTime = event.timestamp; }
time.title = String(event.timestamp || "");
dateCell.append(time, raw(node("span", "log-date", dateParts.date)));
if (dateParts.unset) { dateCell.appendChild(node("span", "log-time-unset", "시간 미설정")); }
row.appendChild(dateCell);
values.slice(1, -1).forEach(function (content, index) {
const column = index + 1;
const cell = node("td");
const tone = String(kind === "system" ? event.severity : kind === "port" ? event.direction : event.state || "unknown").toLowerCase();
if (kind === "system" && column === 1 || kind !== "system" && column === 3) {
cell.appendChild(badge(content, tone));
row.dataset.tone = tone;
} else if (kind !== "system" && column === 1) {
cell.appendChild(raw(node("span", "log-channel", content)));
} else {
const label = node("span", column === 2 ? "log-category" : "log-source");
if (kind === "port" && column === 4) { raw(label); }
highlighted(label, content, keyword);
cell.appendChild(label);
}
row.appendChild(cell);
});
const messageCell = node("td", "log-message-cell");
if (kind === "system" && event.audit_protected === true) { messageCell.appendChild(badge("감사 기록 · 보존", "info")); }
const details = node("details", "log-message-details");
const summary = node("summary");
const preview = raw(node("span", "log-message-preview"));
highlighted(preview, values[values.length - 1], keyword);
summary.append(preview, node("span", "log-detail-hint", "상세"));
const detail = node("div", "log-event-detail");
const metadata = node("dl", "log-event-metadata");
const fields = [["원본 시각", event.timestamp], ["발생 위치", event.source], ["사용자", event.actor], ["작업", event.action], ["대상", event.target], ["원격 주소", event.remote_address ? event.remote_address + (event.remote_port ? ":" + event.remote_port : "") : null], ["감사 ID", event.audit_id]];
fields.forEach(function (field) {
if (field[1] === null || field[1] === undefined || field[1] === "") { return; }
const pair = node("div");
pair.append(node("dt", "", field[0]), raw(node("dd", "", field[1])));
metadata.appendChild(pair);
});
detail.appendChild(metadata);
[["원본 메시지", event.message], ["명령", event.command], ["결과", event.return_value]].forEach(function (field) {
if (field[1] === null || field[1] === undefined || field[1] === "") { return; }
detail.append(node("h3", "", field[0]), raw(node("pre", "", field[1])));
});
// Never decode or reveal payload_base64; preserve the API's redacted text.
if (event.redacted) { detail.appendChild(badge("마스킹 적용", "neutral")); }
details.append(summary, detail);
messageCell.appendChild(details);
row.appendChild(messageCell);
return row;
}
function showEmpty(label, hint) {
const row = node("tr");
const cell = node("td", "empty-state log-empty-state");
cell.colSpan = table.querySelectorAll("thead th").length;
cell.append(node("strong", "", label), node("p", "", hint));
row.appendChild(cell);
tableBody.replaceChildren(row);
}
showEmpty("로그를 불러오는 중입니다.", "장비에 저장된 로그를 조회하고 있습니다.");
return {
busyReading: function () { return Boolean(tableBody.querySelector("details[open]")) || panel.querySelector(".table-wrap").scrollTop > 8; },
render: function (events, rows, timezone, filter) {
hasResult = true;
const breakdownRows = summaryFor(kind, events);
const nextSummary = JSON.stringify(breakdownRows);
if (previousSummary !== nextSummary) {
breakdown.replaceChildren();
breakdownRows.forEach(function (item) {
const chip = badge(item.label, item.key);
chip.appendChild(raw(node("strong", "", item.count)));
breakdown.appendChild(chip);
});
previousSummary = nextSummary;
}
totalNumber.textContent = String(events.length);
const limit = Number(filter.limit) || 100;
scope.textContent = events.length >= limit ? "최대 " + limit + "건 표시 · 전체 저장 건수가 아닙니다." : "현재 조회된 로그 기준";
const time = updated.querySelector("time");
const now = new Date();
time.dateTime = now.toISOString();
time.textContent = now.toTimeString().slice(0, 8);
if (!events.length) {
previousRows.clear();
showEmpty("조건에 맞는 로그가 없습니다.", "검색 조건을 바꾸거나 초기화를 눌러 다시 조회하세요.");
return;
}
const nextRows = new Map();
const occurrences = new Map();
events.forEach(function (event, index) {
const signature = JSON.stringify([event, timezone, filter.keyword || ""]);
const occurrence = occurrences.get(signature) || 0;
occurrences.set(signature, occurrence + 1);
const key = signature + ":" + occurrence;
const row = previousRows.get(key) || makeRow(event, rows[index], timezone, filter.keyword);
nextRows.set(key, row);
});
// Retain row nodes so an unchanged refresh does not reset open details.
const retained = new Set(nextRows.values());
Array.from(tableBody.children).forEach(function (row) { if (!retained.has(row)) { row.remove(); } });
let cursor = tableBody.firstElementChild;
nextRows.forEach(function (row) {
if (row !== cursor) { tableBody.insertBefore(row, cursor); }
cursor = row.nextElementSibling;
});
previousRows = nextRows;
},
state: function (state) {
panel.dataset.loadState = state;
table.setAttribute("aria-busy", String(state === "loading"));
feedback.hidden = !["error", "reading"].includes(state);
feedback.dataset.tone = state;
feedback.textContent = state === "error" ? (hasResult ? "조회에 실패했습니다. 마지막으로 받은 결과를 유지합니다. 새로고침으로 다시 시도하세요." : "로그를 조회할 수 없습니다. 새로고침으로 다시 시도하세요.")
: state === "reading" ? "로그를 읽는 동안 자동 갱신을 기다립니다. 상세를 닫고 목록 맨 위로 이동하면 다시 갱신됩니다." : "";
if (!hasResult && state === "error") {
showEmpty("로그를 조회할 수 없습니다.", "장비 연결 상태를 확인한 후 새로고침을 눌러주세요.");
} else if (!hasResult && state === "loading") {
showEmpty("로그를 불러오는 중입니다.", "장비에 저장된 로그를 조회하고 있습니다.");
}
}
};
}
api.mount = mount;
window.teraLogView = api;
}());