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>
 |