LogAnalyze/public/index.html

523 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>PID Log Viewer</title>
<style>
* {
box-sizing: border-box;
}
:root {
--primary: #1e88e5;
--primary-light: #e3f2fd;
--gray-light: #f8f9fa;
--gray-border: #e0e0e0;
--text-color: #333;
--bg-color: #ffffff;
}
body {
margin: 0;
font-family: "Segoe UI", Roboto, sans-serif;
display: flex;
height: 100vh;
background-color: var(--bg-color);
color: var(--text-color);
}
.sidebar {
width: 280px;
background-color: var(--gray-light);
border-right: 1px solid var(--gray-border);
display: flex;
flex-direction: column;
padding: 0;
}
.sidebar-header {
padding: 16px;
background: white;
border-bottom: 1px solid #ddd;
flex-shrink: 0;
z-index: 1;
}
.sidebar-list {
flex: 1;
overflow-y: auto;
padding: 12px 16px;
}
.pid-button {
display: block;
width: 100%;
padding: 8px 12px;
margin: 4px 0;
text-align: left;
border: 1px solid transparent;
background: #fff;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 14px;
}
.pid-button:hover {
background-color: var(--primary-light);
border-color: var(--primary);
color: var(--primary);
}
.pid-button.selected {
background-color: var(--primary);
color: white;
}
.content {
flex: 1;
padding: 24px;
overflow-y: auto;
background-color: #fefefe;
}
select,
input[type="text"] {
width: 100%;
padding: 6px;
/* margin-bottom: 16px; */
border: 1px solid #ccc;
border-radius: 6px;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
}
select:focus,
input[type="text"]:focus {
border-color: var(--primary);
}
.log-block {
background: #fafafa;
padding: 12px;
margin-bottom: 12px;
border-left: 4px solid var(--primary);
font-family: monospace;
white-space: pre-wrap;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}
.command-section {
margin-top: 32px;
}
.command-section h3 {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
font-size: 17px;
}
.command-section h3 > div {
display: flex;
align-items: center;
gap: 6px;
}
.next-btn {
font-size: 13px;
background: var(--primary);
color: white;
border: none;
padding: 5px 10px;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
}
.next-btn:hover:not(:disabled) {
background-color: #1565c0;
}
.next-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
table {
border-collapse: collapse;
width: 100%;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
margin-bottom: 20px;
}
table thead {
background: var(--primary);
color: white;
}
table th,
table td {
padding: 12px 14px;
text-align: left;
font-size: 14px;
border-bottom: 1px solid #eee;
}
table tbody tr:hover {
background-color: #f1f8ff;
}
code {
background: #f4f4f4;
padding: 2px 6px;
border-radius: 4px;
font-size: 13px;
}
mark {
background-color: #ffdb38;
padding: 0 2px;
border-radius: 2px;
}
#selectAndVersionDiv {
display: flex;
justify-content: space-between;
align-items: center;
}
#selectedPIDDiv {
width: 23%;
}
#selectedVersionDiv {
width: 53%;
}
.action-btn {
background-color: #8d7df3;
color: white;
border: none;
padding: 8px 14px;
margin: 5px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease, box-shadow 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 123, 255, 0.2);
}
.action-btn:hover {
background-color: #0056b3;
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
}
.action-btn:active {
background-color: #004a99;
transform: scale(0.98);
}
</style>
</head>
<body>
<div class="sidebar">
<div class="sidebar-header">
<h3>PIDs (<span id="pidCount">0</span>)</h3>
<input type="text" id="pidSearch" placeholder="🔍 Search PID..." />
</div>
<div class="sidebar-list" id="pidListQuick"></div>
</div>
<div class="content">
<!-- <h3>📊 PID Log Viewer</h3> -->
<div id="selectAndVersionDiv">
<div id="selectedPIDDiv">
<label for="pidList">PID</label>
<select id="pidList" disabled></select>
</div>
<div id="selectedVersionDiv">
<label for="versionList">Select Version</label>
<select id="versionList"></select>
</div>
<div style="width: 23%; align-items: center">
<button id="copyBtn" class="action-btn">📋 Copy</button>
<button id="downloadBtn" class="action-btn">⬇️ Download</button>
</div>
</div>
<div id="errorTableContainer" class="command-section">
<h3>⚠️ Error History for PID</h3>
<table border="1" cellspacing="0" cellpadding="5" width="100%">
<thead>
<tr>
<th style="text-align: left">Logging</th>
<th style="text-align: left">Pattern</th>
<th style="text-align: left">Description</th>
</tr>
</thead>
<tbody id="errorTableBody"></tbody>
</table>
</div>
<div id="logSections"></div>
</div>
<script>
const pidSearchInput = document.getElementById("pidSearch");
const pidList = document.getElementById("pidList");
const versionList = document.getElementById("versionList");
const pidListQuick = document.getElementById("pidListQuick");
const logSections = document.getElementById("logSections");
const commands = ["inventory", "version", "license", "logging"];
const logsCache = {}; // Store logs and current index for each command
async function loadPIDs() {
const res = await fetch("/api/pids");
const pids = await res.json();
document.getElementById("pidCount").textContent = pids.length;
pidListQuick.innerHTML = "";
pids.forEach((pid) => {
const btn = document.createElement("button");
btn.className = "pid-button";
btn.textContent = pid;
btn.onclick = () => selectPID(pid);
pidListQuick.appendChild(btn);
});
}
async function selectPID(pid) {
document
.querySelectorAll(".pid-button")
.forEach((b) => b.classList.remove("selected"));
const target = [...document.querySelectorAll(".pid-button")].find(
(b) => b.textContent === pid
);
if (target) target.classList.add("selected");
const res = await fetch(`/api/pid/${encodeURIComponent(pid)}/versions`);
const versions = await res.json();
pidList.innerHTML = `<option value="${pid}">${pid}</option>`;
versionList.innerHTML = versions
.map((v) => {
const cmds = Object.entries(v.commands)
.filter(([_, count]) => count > 0)
.map(([cmd, count]) => `${cmd} (${count})`)
.join(", ");
return `<option value="${v.version}">${v.version} - ${
cmds || "No commands"
}</option>`;
})
.join("");
// ✅ Default chọn version đầu tiên
if (versions.length > 0) {
versionList.value = versions[0].version;
versionList.dispatchEvent(new Event("change"));
}
await loadErrorTable(pid);
}
pidSearchInput.addEventListener("input", async () => {
const keyword = pidSearchInput.value.toLowerCase().trim();
const res = await fetch("/api/pids");
const pids = await res.json();
const filtered = pids.filter((pid) =>
pid.toLowerCase().includes(keyword)
);
pidListQuick.innerHTML = "";
filtered.forEach((pid) => {
const btn = document.createElement("button");
btn.className = "pid-button";
btn.textContent = pid;
btn.onclick = () => selectPID(pid);
pidListQuick.appendChild(btn);
});
pidList.innerHTML = filtered
.map((pid) => `<option value="${pid}">${pid}</option>`)
.join("");
if (filtered.length > 0) {
pidList.value = filtered[0];
selectPID(filtered[0]);
}
});
pidList.addEventListener("change", () => {
const pid = pidList.value;
if (pid) selectPID(pid);
});
versionList.addEventListener("change", () => {
const pid = pidList.value;
const version = versionList.value;
if (pid && version) loadLogs(pid, version);
});
async function loadLogs(pid, version) {
logSections.innerHTML = "";
logsCache[`${pid}_${version}`] = {};
for (const cmd of commands) {
const res = await fetch(
`/api/device/${encodeURIComponent(pid)}/${encodeURIComponent(
version
)}/${cmd}`
);
const data = await res.json();
if (!data || data.length === 0) continue;
logsCache[`${pid}_${version}`][cmd] = {
logs: data,
index: 0,
};
const section = document.createElement("div");
section.className = "command-section";
section.id = `section-${cmd}`;
section.innerHTML = `
<h3>
${"show " + cmd} (${data.length})
<div>
<button class="next-btn" id="btn-prev-${cmd}">Previous</button>
<span id="page-info-${cmd}">Output 1/${data.length}</span>
<button class="next-btn" id="btn-next-${cmd}">Next</button>
</div>
</h3>
<div class="log-block" id="log-${cmd}">
<b>${data[0].filename}</b>\n${data[0].output}
</div>
`;
logSections.appendChild(section);
// Navigation logic
const updateDisplay = () => {
const info = logsCache[`${pid}_${version}`][cmd];
const curr = info.logs[info.index];
document.getElementById(
`log-${cmd}`
).innerHTML = `<b>${curr.filename}</b>\n${curr.output}`;
document.getElementById(`page-info-${cmd}`).textContent = `Output ${
info.index + 1
}/${info.logs.length}`;
document.getElementById(`btn-prev-${cmd}`).disabled =
info.index === 0;
};
document
.getElementById(`btn-next-${cmd}`)
.addEventListener("click", () => {
const info = logsCache[`${pid}_${version}`][cmd];
info.index = (info.index + 1) % info.logs.length;
updateDisplay();
});
document
.getElementById(`btn-prev-${cmd}`)
.addEventListener("click", () => {
const info = logsCache[`${pid}_${version}`][cmd];
info.index =
(info.index - 1 + info.logs.length) % info.logs.length;
updateDisplay();
});
}
}
loadPIDs();
const copyBtn = document.getElementById("copyBtn");
const downloadBtn = document.getElementById("downloadBtn");
function getExportText() {
const pid = pidList.value || "";
const version = versionList.value || "";
const inventory = getCommandOutput("inventory");
const versionOut = getCommandOutput("version");
const logging = getCommandOutput("logging");
const license = getCommandOutput("license");
return `PID: ${pid}
Version: ${version}
${inventory}
${versionOut}
${logging}
${license}`.trim();
}
// Assume you store outputs in memory or fetch based on UI state
function getCommandOutput(command) {
const pid = pidList.value;
const version = versionList.value;
const key = `${pid}_${version}`;
if (
logsCache[key] &&
logsCache[key][command] &&
logsCache[key][command].logs.length > 0
) {
const { logs, index } = logsCache[key][command];
return logs[index].output || "";
}
return "No data";
}
copyBtn.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(getExportText());
alert("Copied to clipboard!");
} catch (err) {
alert("Failed to copy.");
}
});
downloadBtn.addEventListener("click", () => {
const content = getExportText();
const blob = new Blob([content], { type: "text/plain" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `log-${pidList.value || "unknown"}.txt`;
a.click();
});
async function loadErrorTable(pid) {
const res = await fetch(`/api/errors/${encodeURIComponent(pid)}`);
const data = await res.json();
const tbody = document.getElementById("errorTableBody");
tbody.innerHTML = "";
if (data.length === 0) {
tbody.innerHTML = `<tr><td colspan="5">✅ No known issues for this PID</td></tr>`;
return;
}
for (const err of data) {
const tr = document.createElement("tr");
tr.innerHTML = `
<td><code>${err.highlighted_message}</code></td>
<td><code>${err.regex_pattern}</code></td>
<td><code>${err.description}</code></td>
`;
tbody.appendChild(tr);
}
}
</script>
</body>
</html>