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