523 lines
14 KiB
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>
|