646 lines
18 KiB
HTML
646 lines
18 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">
|
|
<div id="confirm-count-user" class="item"></div>
|
|
|
|
<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>
|
|
|
|
<div>
|
|
<button
|
|
id="deviceConfirmBtn"
|
|
type="button"
|
|
class="primary-btn"
|
|
>
|
|
Confirm
|
|
</button>
|
|
<span
|
|
id="confirmUserText"
|
|
style="font-size: 12px; display: none"
|
|
>
|
|
Confirmed by <strong id="confirmedUserName"></strong>
|
|
</span>
|
|
</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 id="logDeleteModal" class="modal hidden">
|
|
<div style="max-width: 30%" class="modal-content">
|
|
<div id="deleteLogText"></div>
|
|
<div class="modal-actions">
|
|
<button id="logCancelDeleteBtn" class="secondary-btn">
|
|
Cancel
|
|
</button>
|
|
<button id="logDeleteBtn" class="danger-btn">
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="deviceConfirmModal" class="modal hidden">
|
|
<div style="max-width: 30%" class="modal-content">
|
|
<div id="deviceConfirmText"></div>
|
|
<div class="modal-actions">
|
|
<button id="deviceCancelBtn" class="secondary-btn">
|
|
Cancel
|
|
</button>
|
|
<button id="submitDeviceConfirmBtn" class="danger-btn">
|
|
Confirm
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Authentication and get user info -->
|
|
<script>
|
|
if (!localStorage.getItem("token")) {
|
|
logout();
|
|
}
|
|
|
|
function logout() {
|
|
localStorage.removeItem("token");
|
|
localStorage.removeItem("user");
|
|
window.location.href = "/login";
|
|
}
|
|
|
|
async function getProfile() {
|
|
const token = localStorage.getItem("token");
|
|
|
|
await fetch("/api/profile", {
|
|
method: "GET",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
})
|
|
.then((res) => res.json())
|
|
.then((data) => {
|
|
document.getElementById(
|
|
"welcome-user",
|
|
).innerHTML = `Welcome, <strong>${data?.user?.name}</strong> ▼`;
|
|
|
|
document.getElementById(
|
|
"confirm-count-user",
|
|
).innerHTML = `Confirmed: <strong>${data?.user?.confirm_count}</strong>`;
|
|
})
|
|
.catch((err) => {
|
|
console.error(err);
|
|
});
|
|
}
|
|
|
|
getProfile();
|
|
</script>
|
|
|
|
<!-- Get PID - Version - Log -->
|
|
<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 confirmUserText = document.getElementById("confirmUserText");
|
|
const confirmedUserName =
|
|
document.getElementById("confirmedUserName");
|
|
|
|
const logsCache = {}; // Store logs and current index for each command
|
|
let versionsUser = [];
|
|
|
|
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();
|
|
|
|
versionsUser = versions
|
|
.filter((verItem) => verItem.user)
|
|
.map((verItem) => {
|
|
return {
|
|
version: verItem.version,
|
|
user: verItem.user,
|
|
};
|
|
});
|
|
|
|
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()}`,
|
|
);
|
|
|
|
// Render Confirm Section
|
|
const currentVerUser = versionsUser.find(
|
|
(verItem) => verItem.version === version,
|
|
);
|
|
if (currentVerUser) {
|
|
deviceConfirmBtn.style.display = "none";
|
|
confirmedUserName.textContent =
|
|
currentVerUser.user.name;
|
|
confirmUserText.style.display = "inline";
|
|
} else {
|
|
deviceConfirmBtn.style.display = "inline-block";
|
|
confirmUserText.style.display = "none";
|
|
}
|
|
|
|
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>
|
|
|
|
<!-- Handle delete log -->
|
|
<script>
|
|
let pendingDeleteLog = null;
|
|
|
|
document.addEventListener("click", async (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];
|
|
|
|
pendingDeleteLog = { id: log.id, command, pid, version };
|
|
|
|
const text = `Are you sure you want to delete this log? <br /><code>${pid} - ${version} - ${command}</code> <br /> <div style="font-size: 12px; font-weight: 700" >${log.filename}</div>`;
|
|
document.getElementById("deleteLogText").innerHTML = text;
|
|
showDeleteModal();
|
|
}
|
|
});
|
|
|
|
const logDeleteModal = document.getElementById("logDeleteModal");
|
|
const logCancelDeleteBtn =
|
|
document.getElementById("logCancelDeleteBtn");
|
|
const logDeleteBtn = document.getElementById("logDeleteBtn");
|
|
|
|
function showDeleteModal() {
|
|
logDeleteModal.classList.remove("hidden");
|
|
}
|
|
function hideDeleteModal() {
|
|
logDeleteModal.classList.add("hidden");
|
|
pendingDeleteLog = null;
|
|
}
|
|
|
|
logCancelDeleteBtn.addEventListener("click", hideDeleteModal);
|
|
|
|
logDeleteBtn.addEventListener("click", async () => {
|
|
if (!pendingDeleteLog) return;
|
|
|
|
const token = localStorage.getItem("token");
|
|
try {
|
|
const res = await fetch("/api/delete-log", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify(pendingDeleteLog),
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
alert("Deleted successfully!");
|
|
location.reload();
|
|
} else {
|
|
alert("Delete failed.");
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert("Delete failed.");
|
|
} finally {
|
|
hideDeleteModal();
|
|
pendingDeleteLog = null;
|
|
}
|
|
});
|
|
|
|
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>
|
|
|
|
<!-- Handle confirm device -->
|
|
<script>
|
|
const deviceConfirmBtn =
|
|
document.getElementById("deviceConfirmBtn");
|
|
const deviceConfirmModal =
|
|
document.getElementById("deviceConfirmModal");
|
|
const deviceCancelBtn = document.getElementById("deviceCancelBtn");
|
|
const submitDeviceConfirmBtn = document.getElementById(
|
|
"submitDeviceConfirmBtn",
|
|
);
|
|
const deviceConfirmText =
|
|
document.getElementById("deviceConfirmText");
|
|
|
|
let pendingDevice = null;
|
|
|
|
deviceConfirmBtn.addEventListener("click", () => {
|
|
const pid = document.getElementById("pidList").value;
|
|
const version = document.getElementById("versionList").value;
|
|
|
|
if (!pid || !version) {
|
|
alert("PID or version is missing.");
|
|
return;
|
|
}
|
|
|
|
pendingDevice = { pid, version };
|
|
|
|
deviceConfirmText.innerHTML = `
|
|
Are you sure you want to confirm this device?<br />
|
|
<strong>${pid} - ${version}</strong>
|
|
`;
|
|
|
|
deviceConfirmModal.classList.remove("hidden");
|
|
});
|
|
|
|
deviceCancelBtn.addEventListener("click", () => {
|
|
deviceConfirmModal.classList.add("hidden");
|
|
pendingDevice = null;
|
|
});
|
|
|
|
submitDeviceConfirmBtn.addEventListener("click", async () => {
|
|
if (!pendingDevice) return;
|
|
|
|
const token = localStorage.getItem("token");
|
|
try {
|
|
const res = await fetch("/api/confirm-device", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify(pendingDevice),
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
alert(data.message || "Device confirmed successfully!");
|
|
location.reload();
|
|
} else {
|
|
alert(data.message || "Confirmation failed.");
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert("Confirmation failed.");
|
|
} finally {
|
|
deviceConfirmModal.classList.add("hidden");
|
|
pendingDevice = null;
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|