Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <title>Past Analyzes - CTRL + ALT + HEAL</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="stylesheet" href="style.css"></script> | |
| <script src="script.js"></script> | |
| </head> | |
| <body class="bg-[#F7F8F9] min-h-screen"> | |
| <nav | |
| class="fixed top-0 left-0 w-full z-50 backdrop-blur-md bg-white/20 border-b border-white/30 shadow-md" | |
| > | |
| <div class="flex justify-between items-center w-full px-6 py-4"> | |
| <a | |
| href="index.html" | |
| class="text-2xl font-bold text-black hover:text-[var(--tropical-indigo)] transition" | |
| > | |
| CTRL + ALT + HEAL | |
| </a> | |
| <ul class="hidden md:flex space-x-6 font-medium text-gray-800"> | |
| <li> | |
| <a | |
| href="profile.html" | |
| class="block text-gray-800 hover:text-[var(--tropical-indigo)]" | |
| >Profile</a | |
| > | |
| </li> | |
| <li> | |
| <a | |
| href="analyzer.html" | |
| class="block text-gray-800 hover:text-[var(--tropical-indigo)]" | |
| >Analyzer</a | |
| > | |
| </li> | |
| <li> | |
| <a | |
| href="past_data.html" | |
| class="block text-gray-800 hover:text-[var(--tropical-indigo)]" | |
| >Past Reports</a | |
| > | |
| </li> | |
| <li id="authNavItem"> | |
| <a | |
| href="login.html" | |
| class="block text-gray-800 hover:text-[var(--tropical-indigo)]" | |
| >Login</a | |
| > | |
| </li> | |
| </ul> | |
| </div> | |
| </nav> | |
| <main class="container mx-auto px-6 pt-24"> | |
| <h2 class="text-xl font-semibold mb-4 text-sky-700"> | |
| Your Past Analyzes | |
| </h2> | |
| <p id="auth-status" class="text-sm text-gray-500 mb-6"> | |
| Checking sign-in status… | |
| </p> | |
| <div id="recs-container" class="space-y-4"> | |
| <p class="text-sm text-gray-500"></p> | |
| </div> | |
| </main> | |
| <script type="module"> | |
| import { initializeApp } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-app.js"; | |
| import { | |
| getAuth, | |
| onAuthStateChanged, | |
| signOut, | |
| } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-auth.js"; | |
| import { | |
| getFirestore, | |
| collection, | |
| query, | |
| orderBy, | |
| getDocs, | |
| } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore.js"; | |
| const firebaseConfig = { | |
| apiKey: "AIzaSyAPhM_Ee7cLzyKHs5zyFy8g5ZOk9-pubRI", | |
| authDomain: "login-tutorial-7a9e1.firebaseapp.com", | |
| projectId: "login-tutorial-7a9e1", | |
| storageBucket: "login-tutorial-7a9e1.firebasestorage.app", | |
| messagingSenderId: "491093197824", | |
| appId: "1:491093197824:web:9f86659034af7e6a8244e5", | |
| measurementId: "G-JM7T9N6ZLZ", | |
| }; | |
| const app = initializeApp(firebaseConfig); | |
| const auth = getAuth(app); | |
| const db = getFirestore(app); | |
| let currentUser = null; | |
| onAuthStateChanged(auth, (user) => { | |
| const authNavItem = document.getElementById("authNavItem"); | |
| if (authNavItem) { | |
| if (user) { | |
| authNavItem.innerHTML = | |
| '<button onclick="logout()" class="hover:text-[#6B9080] text-red-600">Logout</button>'; | |
| } else { | |
| authNavItem.innerHTML = | |
| '<a href="login.html" class="hover:text-[#6B9080]">Login</a>'; | |
| } | |
| } | |
| }); | |
| window.logout = async () => { | |
| try { | |
| await signOut(auth); | |
| localStorage.clear(); | |
| window.location.href = "login.html"; | |
| } catch (error) { | |
| console.error("Error signing out:", error); | |
| } | |
| }; | |
| const statusEl = document.getElementById("auth-status"); | |
| const recsEl = document.getElementById("recs-container"); | |
| function dbg(label, data) { | |
| console.log( | |
| `[PastReports][${new Date().toISOString()}] ${label}:`, | |
| data | |
| ); | |
| } | |
| const SHOW_DEBUG = false; | |
| let debugBannerEl = null; | |
| if (SHOW_DEBUG) { | |
| debugBannerEl = document.createElement("div"); | |
| debugBannerEl.style.cssText = | |
| "font-size:12px;margin-top:6px;color:#6b7280;"; | |
| statusEl.after(debugBannerEl); | |
| } | |
| function setDebugBanner(msg) { | |
| if (debugBannerEl) debugBannerEl.textContent = `Debug: ${msg}`; | |
| } | |
| window.toggleBox = function (id) { | |
| const content = document.getElementById(`box-content-${id}`); | |
| const arrow = document.getElementById(`box-arrow-${id}`); | |
| if (!content || !arrow) return; | |
| const open = | |
| content.style.maxHeight && content.style.maxHeight !== "0px"; | |
| if (open) { | |
| content.style.maxHeight = "0px"; | |
| arrow.style.transform = "rotate(0deg)"; | |
| } else { | |
| content.style.maxHeight = content.scrollHeight + "px"; | |
| arrow.textContent = "Close Full View"; | |
| } | |
| }; | |
| function safeJson(x) { | |
| try { | |
| return typeof x === "string" ? JSON.parse(x) : x; | |
| } catch { | |
| return x; | |
| } | |
| } | |
| function topAnomalyLabels(anoms, n = 3) { | |
| const arr = Array.isArray(anoms) ? anoms : []; | |
| return arr | |
| .slice(0, n) | |
| .map((a) => | |
| (a?.findings ?? a?.condition ?? a?.measurement ?? "").toString() | |
| ) | |
| .filter(Boolean); | |
| } | |
| function deriveReportName(row) { | |
| if (row.filename) return row.filename; | |
| if (row.report_date) | |
| return `Report ${new Date(row.report_date).toLocaleDateString()}`; | |
| if (row.ocr_text) { | |
| const first = row.ocr_text.split(/\r?\n/).find((l) => l.trim()); | |
| if (first) return first.slice(0, 50) + (first.length > 50 ? "…" : ""); | |
| } | |
| return "Report"; | |
| } | |
| function renderBackendReportCollapsible(row, idx) { | |
| const id = `r${row.id || idx}`; | |
| const created = row.created_at | |
| ? new Date(row.created_at).toLocaleString() | |
| : ""; | |
| const reportName = deriveReportName(row); | |
| const anomaliesRaw = safeJson(row.anomalies); | |
| const anomalies = Array.isArray(anomaliesRaw) ? anomaliesRaw : []; | |
| const top3 = topAnomalyLabels(anomalies, 3); | |
| return ` | |
| <div class="bg-white border border-gray-200 rounded-lg shadow"> | |
| <!-- Header (clickable) --> | |
| <button | |
| class="w-full text-left p-4 flex items-start justify-between gap-3 hover:bg-gray-50 rounded-t-lg" | |
| onclick="toggleBox('${id}')" | |
| > | |
| <div class="min-w-0"> | |
| <div class="text-xs text-gray-500">${created || "—"}</div> | |
| <div class="font-semibold text-sky-700 truncate">${reportName}</div> | |
| ${ | |
| top3.length | |
| ? `<div class="flex flex-wrap gap-2 mt-2"> | |
| ${top3 | |
| .map( | |
| (t) => ` | |
| <span class="text-xs bg-gray-100 border border-gray-200 px-2 py-1 rounded"> | |
| ${t} | |
| </span> | |
| ` | |
| ) | |
| .join("")} | |
| </div>` | |
| : `<div class="text-xs text-gray-400 mt-2">No anomalies detected</div>` | |
| } | |
| </div> | |
| <span id="box-arrow-${id}" class="text-gray-500 transition-transform select-none mt-1">Open Full View</span> | |
| </button> | |
| <!-- Collapsible body --> | |
| <div | |
| id="box-content-${id}" | |
| class="overflow-hidden transition-all duration-300 max-h-0 border-t border-gray-200" | |
| > | |
| <div class="p-4 space-y-3"> | |
| <!-- Full anomalies list --> | |
| ${ | |
| anomalies.length | |
| ? ` | |
| <div class="pt-2 border-t border-gray-100"> | |
| <div class="font-medium mb-2">Findings</div> | |
| ${anomalies | |
| .map( | |
| (a, i) => ` | |
| <div class="border-t border-gray-200 pt-2 mt-2"> | |
| <div class="font-medium">Finding ${i + 1}: ${ | |
| a?.findings ?? "" | |
| }</div> | |
| <div class="text-sm text-gray-600">Severity: ${ | |
| a?.severity ?? "" | |
| }</div> | |
| <div class="text-sm text-gray-600">Recommendations: ${(Array.isArray( | |
| a?.recommendations | |
| ) | |
| ? a.recommendations | |
| : [] | |
| ).join(", ")}</div> | |
| </div> | |
| ` | |
| ) | |
| .join("")} | |
| </div> | |
| ` | |
| : "" | |
| } | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function renderFirestoreReportCollapsible(item, idx) { | |
| const id = `f${idx}`; | |
| const created = | |
| item?.createdAt && item.createdAt.toDate | |
| ? item.createdAt.toDate().toLocaleString() | |
| : ""; | |
| const reportName = item?.reportDate | |
| ? `Report ${item.reportDate}` | |
| : "Report"; | |
| const anomalies = Array.isArray(item?.resolutions) | |
| ? item.resolutions | |
| : []; | |
| const top3 = topAnomalyLabels(anomalies, 3); | |
| return ` | |
| <div class="bg-white border border-gray-200 rounded-lg shadow"> | |
| <!-- Header (clickable) --> | |
| <button | |
| class="w-full text-left p-4 flex items-start justify-between gap-3 hover:bg-gray-50 rounded-t-lg" | |
| onclick="toggleBox('${id}')" | |
| > | |
| <div class="min-w-0"> | |
| <div class="text-xs text-gray-500">${created || "—"}</div> | |
| <div class="font-semibold text-blue-700 truncate">${reportName}</div> | |
| ${ | |
| top3.length | |
| ? `<div class="flex flex-wrap gap-2 mt-2"> | |
| ${top3 | |
| .map( | |
| (t) => ` | |
| <span class="text-xs bg-gray-100 border border-gray-200 px-2 py-1 rounded"> | |
| ${t} | |
| </span>` | |
| ) | |
| .join("")} | |
| </div>` | |
| : `<div class="text-xs text-gray-400 mt-2">No anomalies detected</div>` | |
| } | |
| </div> | |
| <span id="box-arrow-${id}" class="text-gray-500 transition-transform select-none mt-1">Open Full View</span> | |
| </button> | |
| <!-- Collapsible body --> | |
| <div | |
| id="box-content-${id}" | |
| class="overflow-hidden transition-all duration-300 max-h-0 border-t border-gray-200" | |
| > | |
| <div class="p-4 space-y-3"> | |
| <!-- OCR --> | |
| <!-- Full anomalies list --> | |
| ${ | |
| anomalies.length | |
| ? ` | |
| <div class="pt-2 border-t border-gray-100"> | |
| <div class="font-medium mb-2">Findings</div> | |
| ${anomalies | |
| .map( | |
| (a, i) => ` | |
| <div class="border-t border-gray-200 pt-2 mt-2"> | |
| <div class="font-medium">Finding ${i + 1}: ${ | |
| a?.findings ?? "" | |
| }</div> | |
| <div class="text-sm text-gray-600">Severity: ${ | |
| a?.severity ?? "" | |
| }</div> | |
| <div class="text-sm text-gray-600">Recommendations: ${ | |
| Array.isArray(a?.recommendations) | |
| ? a.recommendations.join(", ") | |
| : "" | |
| }</div> | |
| </div>` | |
| ) | |
| .join("")} | |
| </div> | |
| ` | |
| : "" | |
| } | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function renderBackendRow(row) { | |
| const created = row.created_at | |
| ? new Date(row.created_at).toLocaleString() | |
| : ""; | |
| const reportDate = row.report_date || "N/A"; | |
| const anomalies = (() => { | |
| try { | |
| return Array.isArray(row.anomalies) | |
| ? row.anomalies | |
| : JSON.parse(row.anomalies || "[]"); | |
| } catch { | |
| return []; | |
| } | |
| })(); | |
| return ` | |
| <div class="bg-white border border-gray-200 rounded-lg p-4 shadow"> | |
| <div class="flex justify-between mb-2"> | |
| <div class="font-semibold text-blue-700">Report: ${reportDate}</div> | |
| <div class="text-xs text-gray-500">${created}</div> | |
| </div> | |
| <pre class="whitespace-pre-wrap text-sm text-gray-700 mb-2">${ | |
| row.ocr_text || "" | |
| }</pre> | |
| ${anomalies | |
| .map( | |
| (r, i) => ` | |
| <div class="border-t border-gray-200 pt-2 mt-2"> | |
| <div class="font-medium">Finding ${i + 1}: ${ | |
| r?.findings ?? "" | |
| }</div> | |
| <div class="text-sm text-gray-600">Severity: ${ | |
| r?.severity ?? "" | |
| }</div> | |
| <div class="text-sm text-gray-600">Recommendations: ${(Array.isArray( | |
| r?.recommendations | |
| ) | |
| ? r.recommendations | |
| : [] | |
| ).join(", ")}</div> | |
| </div> | |
| ` | |
| ) | |
| .join("")} | |
| </div>`; | |
| } | |
| function renderList(htmlItems) { | |
| recsEl.innerHTML = | |
| htmlItems && htmlItems.length | |
| ? htmlItems.join("") | |
| : '<p class="text-sm text-gray-500">No saved analyses yet.</p>'; | |
| } | |
| async function loadFromBackend(user) { | |
| try { | |
| const userId = user.email || user.uid; | |
| const url = api('reports/', { user_id: userId }); | |
| dbg("Backend URL", url); | |
| const res = await fetch(url, { method: "GET" }); | |
| dbg("Backend HTTP status", res.status); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const data = await res.json(); | |
| dbg("Backend payload", data); | |
| return Array.isArray(data) ? data : []; | |
| } catch (e) { | |
| console.error("Backend fetch failed:", e); | |
| dbg("Backend fetch error", e.message || e); | |
| return []; | |
| } | |
| } | |
| async function loadFromFirestore(user) { | |
| try { | |
| const q = query( | |
| collection(db, "users", user.uid, "analyses"), | |
| orderBy("createdAt", "desc") | |
| ); | |
| dbg("Firestore query uid", user.uid); | |
| const snap = await getDocs(q); | |
| const rows = snap.docs.map((d) => d.data()); | |
| dbg("Firestore count", rows.length); | |
| return rows; | |
| } catch (e) { | |
| console.error("Firestore fetch failed:", e); | |
| dbg("Firestore fetch error", e.message || e); | |
| return []; | |
| } | |
| } | |
| onAuthStateChanged(auth, async (user) => { | |
| if (!user) { | |
| statusEl.textContent = "Not signed in."; | |
| renderList([]); | |
| setDebugBanner("User is not signed in."); | |
| return; | |
| } | |
| statusEl.textContent = `Signed in as ${user.email || user.uid}`; | |
| const backendRows = await loadFromBackend(user); | |
| const firestoreRows = await loadFromFirestore(user); | |
| setDebugBanner( | |
| `backend: ${backendRows.length} | firestore: ${firestoreRows.length}` | |
| ); | |
| dbg("Summary", { | |
| backend: backendRows.length, | |
| firestore: firestoreRows.length, | |
| }); | |
| if (backendRows.length > 0) { | |
| recsEl.innerHTML = backendRows | |
| .map((row, i) => renderBackendReportCollapsible(row, i)) | |
| .join(""); | |
| } else if (firestoreRows.length > 0) { | |
| recsEl.innerHTML = firestoreRows | |
| .map((row, i) => renderFirestoreReportCollapsible(row, i)) | |
| .join(""); | |
| } else { | |
| recsEl.innerHTML = | |
| '<p class="text-sm text-gray-500">No saved analyses yet.</p>'; | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |