588 lines
16 KiB
HTML
588 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<title>PID Log Viewer</title>
|
|
|
|
<!-- Link to CSS file -->
|
|
<link rel="stylesheet" href="/css/style.css" />
|
|
</head>
|
|
<body>
|
|
<div id="logToast" class="toast hidden"></div>
|
|
|
|
<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
|
|
style="
|
|
margin-bottom: 0.5rem;
|
|
display: flex;
|
|
justify-content: end;
|
|
"
|
|
>
|
|
<div class="user-dropdown">
|
|
<button
|
|
id="welcome-user"
|
|
class="user-dropdown-toggle"
|
|
></button>
|
|
|
|
<div class="user-dropdown-content">
|
|
<button onclick="logout()">Logout</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<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>
|
|
|
|
<button
|
|
id="logConfirmDeleteBtn"
|
|
type="button"
|
|
class="btn-with-badge danger-btn"
|
|
>
|
|
Delete
|
|
<span class="badge" id="logDeleteBadge">0</span>
|
|
</button>
|
|
</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>
|
|
|
|
<!-- Log Delete Modal -->
|
|
<div id="logDeleteModal" class="modal hidden">
|
|
<div class="modal-content">
|
|
<h2 class="modal-title">Confirm Deletion</h2>
|
|
|
|
<table class="delete-table">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>PID</th>
|
|
<th>Version</th>
|
|
<th>Command</th>
|
|
<th style="text-align: center">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="logDeleteListContainer"></tbody>
|
|
</table>
|
|
|
|
<div class="modal-actions">
|
|
<button id="logCloseModalBtn" class="btn secondary-btn">
|
|
Cancel
|
|
</button>
|
|
<button id="logSubmitDeleteBtn" class="btn danger-btn">
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
if (!localStorage.getItem("token")) {
|
|
logout();
|
|
}
|
|
|
|
document.getElementById(
|
|
"welcome-user",
|
|
).innerHTML = `Welcome, <strong>${
|
|
JSON.parse(localStorage.getItem("user")).name
|
|
}</strong> ▼`;
|
|
|
|
function logout() {
|
|
localStorage.removeItem("token");
|
|
localStorage.removeItem("user");
|
|
window.location.href = "/login";
|
|
}
|
|
</script>
|
|
|
|
<script>
|
|
// Get param pid, version from URL
|
|
const params = new URLSearchParams(window.location.search);
|
|
const pidParam = params.get("pid");
|
|
const versionParam = params.get("version");
|
|
|
|
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(pidParam, versionParam) {
|
|
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);
|
|
});
|
|
|
|
if (pidParam) {
|
|
selectPID(pidParam, versionParam);
|
|
}
|
|
}
|
|
|
|
async function selectPID(pid, versionParam) {
|
|
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("");
|
|
|
|
// Set param pid, version to URL
|
|
const currentParams = new URLSearchParams(
|
|
window.location.search,
|
|
);
|
|
currentParams.set("pid", pid);
|
|
|
|
// Có param version thì chọn
|
|
if (versionParam) {
|
|
versionList.value = versionParam;
|
|
versionList.dispatchEvent(new Event("change"));
|
|
currentParams.set("version", versionParam);
|
|
} else {
|
|
// ✅ Default chọn version đầu tiên
|
|
if (versions.length > 0) {
|
|
versionList.value = versions[0].version;
|
|
versionList.dispatchEvent(new Event("change"));
|
|
currentParams.set("version", versions[0].version);
|
|
}
|
|
}
|
|
|
|
history.replaceState(null, "", `?${currentParams.toString()}`);
|
|
|
|
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) {
|
|
// Set param pid, version to URL
|
|
const currentParams = new URLSearchParams(
|
|
window.location.search,
|
|
);
|
|
currentParams.set("pid", pid);
|
|
currentParams.set("version", version);
|
|
history.replaceState(
|
|
null,
|
|
"",
|
|
`?${currentParams.toString()}`,
|
|
);
|
|
|
|
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}">
|
|
<button type="button" class="danger-btn trash-btn" title="Add to delete list">🗑</button>
|
|
<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];
|
|
|
|
const logBlock = document.getElementById(`log-${cmd}`);
|
|
logBlock.innerHTML = `
|
|
<button type="button" class="danger-btn trash-btn" title="Add to delete list">🗑</button>
|
|
<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(pidParam, versionParam);
|
|
|
|
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>
|
|
|
|
<script>
|
|
const STORAGE_KEY = "logDeleteList";
|
|
const logConfirmDeleteBtn = document.getElementById(
|
|
"logConfirmDeleteBtn",
|
|
);
|
|
const logDeleteModal = document.getElementById("logDeleteModal");
|
|
const logDeleteListContainer = document.getElementById(
|
|
"logDeleteListContainer",
|
|
);
|
|
const logCloseModalBtn =
|
|
document.getElementById("logCloseModalBtn");
|
|
const logSubmitDeleteBtn =
|
|
document.getElementById("logSubmitDeleteBtn");
|
|
|
|
function getDeleteList() {
|
|
return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
|
|
}
|
|
|
|
function saveDeleteList(list) {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
|
|
updateDeleteBtn();
|
|
}
|
|
|
|
function updateDeleteBtn() {
|
|
const deleteList = getDeleteList();
|
|
const badge = document.getElementById("logDeleteBadge");
|
|
badge.textContent = deleteList.length;
|
|
}
|
|
|
|
document.addEventListener("click", (e) => {
|
|
if (e.target.classList.contains("trash-btn")) {
|
|
const section = e.target.closest(".command-section");
|
|
const pid = pidList.value;
|
|
const version = versionList.value;
|
|
const command = section.id.replace("section-", "");
|
|
const index = logsCache[`${pid}_${version}`][command].index;
|
|
const log =
|
|
logsCache[`${pid}_${version}`][command].logs[index];
|
|
|
|
const currentList = getDeleteList();
|
|
if (!currentList.some((item) => item.id === log.id)) {
|
|
currentList.push({
|
|
id: log.id,
|
|
pid,
|
|
version,
|
|
command,
|
|
});
|
|
saveDeleteList(currentList);
|
|
|
|
showToast("Added to delete list");
|
|
}
|
|
}
|
|
});
|
|
|
|
function renderDeleteModal() {
|
|
const deleteList = getDeleteList();
|
|
logDeleteListContainer.innerHTML = "";
|
|
deleteList.forEach((item, i) => {
|
|
const tr = document.createElement("tr");
|
|
tr.innerHTML = `
|
|
<td>${item.id}</td>
|
|
<td>${item.pid}</td>
|
|
<td>${item.version}</td>
|
|
<td>${item.command}</td>
|
|
<td style="text-align: center;"><button type="button" class="danger-btn remove-item-btn" data-index="${i}" title="Remove">🗑</button></td>
|
|
`;
|
|
logDeleteListContainer.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
logDeleteListContainer.addEventListener("click", (e) => {
|
|
if (e.target.classList.contains("remove-item-btn")) {
|
|
const index = parseInt(e.target.getAttribute("data-index"));
|
|
const list = getDeleteList();
|
|
list.splice(index, 1);
|
|
saveDeleteList(list);
|
|
renderDeleteModal();
|
|
}
|
|
});
|
|
|
|
// Open delete modal
|
|
logConfirmDeleteBtn.addEventListener("click", () => {
|
|
renderDeleteModal();
|
|
logDeleteModal.classList.remove("hidden");
|
|
});
|
|
|
|
// Open close modal
|
|
logCloseModalBtn.addEventListener("click", () => {
|
|
logDeleteModal.classList.add("hidden");
|
|
});
|
|
|
|
// Submit delete
|
|
logSubmitDeleteBtn.addEventListener("click", async () => {
|
|
const deleteList = getDeleteList();
|
|
if (deleteList.length === 0) {
|
|
alert("Please select at least one item to delete.");
|
|
return;
|
|
}
|
|
|
|
if (
|
|
confirm(
|
|
`Are you sure you want to delete ${deleteList.length} item(s)?`,
|
|
)
|
|
) {
|
|
const deletedItems = deleteList.map((item) => {
|
|
return {
|
|
id: item.id,
|
|
command: item.command,
|
|
};
|
|
});
|
|
|
|
const token = localStorage.getItem("token");
|
|
|
|
await fetch("/api/delete-logs", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({ items: deletedItems }),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((data) => {
|
|
alert("Deleted successfully!");
|
|
localStorage.removeItem(STORAGE_KEY);
|
|
location.reload();
|
|
})
|
|
.catch((err) => {
|
|
console.error(err);
|
|
alert("Delete failed.");
|
|
});
|
|
}
|
|
});
|
|
|
|
updateDeleteBtn();
|
|
|
|
function showToast(message = "Announce something") {
|
|
const toast = document.getElementById("logToast");
|
|
toast.textContent = message;
|
|
toast.classList.add("show");
|
|
toast.classList.remove("hidden");
|
|
|
|
setTimeout(() => {
|
|
toast.classList.remove("show");
|
|
toast.classList.add("hidden");
|
|
}, 2000);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|