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="bg-white border border-gray-200 px-6 py-4 mb-8"> | |
| <div class="container mx-auto flex justify-between items-center"> | |
| <a href="index.html" class="text-2xl font-bold text-[#6B9080]">CTRL + ALT + HEAL</a> | |
| <ul class="flex space-x-6 text-sm font-medium text-gray-700"> | |
| <li><a href="index.html" class="hover:text-[#6B9080]">Home</a></li> | |
| <li><a href="analyzer.html" class="hover:text-[#6B9080]">Analyzer</a></li> | |
| <li><a href="past_data.html" class="hover:text-[#6B9080]">Past Reports</a></li> | |
| <li><a href="profile.html" class="hover:text-[#6B9080]">Profile</a></li> | |
| <li id="authNavItem"><a href="login.html" class="hover:text-[#6B9080]">Login</a></li> | |
| </ul> | |
| </div> | |
| </nav> | |
| <main class="max-w-4xl mx-auto px-4"> | |
| <h1 class="text-2xl font-bold text-green-700 mb-4">Your Past Analyzes</h1> | |
| <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 = true; | |
| 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-green-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 --> | |
| <div> | |
| <div class="font-medium mb-1">OCR Text</div> | |
| <pre class="whitespace-pre-wrap text-sm text-gray-700 bg-gray-50 border rounded p-3">${row.ocr_text || ""}</pre> | |
| </div> | |
| <!-- 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-green-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 --> | |
| <div> | |
| <div class="font-medium mb-1">OCR Text</div> | |
| <pre class="whitespace-pre-wrap text-sm text-gray-700 bg-gray-50 border rounded p-3">${ | |
| item?.ocr_text || "" | |
| }</pre> | |
| </div> | |
| <!-- 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-green-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> |