LogAnalyze/public/dashboard.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>