diff --git "a/ap.jsx" "b/ap.jsx"
new file mode 100644--- /dev/null
+++ "b/ap.jsx"
@@ -0,0 +1,4118 @@
+import React, { useState, useCallback, useEffect, useRef } from "react";
+import { QUESTIONS } from "./data/questions";
+import LoginPage from "./LoginPage";
+import {
+ getSession, logout, startTesting, terminateTesting, resetTesting,
+ getScores, saveScores, clearScores,
+ saveSessionQuestions, getSessionQuestions, clearSessionQuestions,
+} from "./auth";
+import {
+ dbGetAllUsers, dbUpdateRole, dbResetPassword, dbDeleteUser, dbSignup,
+ dbGetSubmissions, dbSaveSubmission, dbDeleteSubmission,
+ dbGetProfiles, dbSaveProfile, dbDeleteProfile,
+ dbGetProducts, dbSaveProduct, dbDeleteProduct,
+ dbGetUserProducts, dbSetUserProducts,
+ dbGetTasks, dbSaveTask, dbDeleteTask,
+ dbGetTickets, dbSaveTicket, dbDeleteTicket,
+ dbGetNotifications, dbSaveNotification, dbMarkNotificationRead, dbMarkAllNotificationsRead, dbClearNotifications,
+ dbAppendLog, dbReadLogs, dbPruneLogs,
+} from "./db";
+import { driveEnabled, uploadPhoto, deleteSubmissionPhotos } from "./drive";
+import logo from "./assets/logo.png";
+import "./App.css";
+
+// ── Constants ─────────────────────────────────────────────────────────────────
+const SESSION_DURATION = 12 * 60 * 60 * 1000;
+const TESTER_DURATION = 60 * 60 * 1000;
+
+function getDuration(s) {
+ return s?.mode === "tester" ? TESTER_DURATION : SESSION_DURATION;
+}
+
+const CATEGORY_COLORS = {
+ FUNC: "#4f86c6", UI: "#6bbf8e", API: "#e08c4a", DATA: "#9b73c8",
+ PERF: "#e05c5c", SEC: "#c8a73a", INT: "#4ab8c8", REG: "#888",
+};
+
+// Fixed "random-looking" preset marks for the default question set
+const PRESET_MARKS = [10,15,10,20,15,10,10,15,10,20,15,10,10,20,15,10,15,20,10,15,20,10,15,10,15,20,10,15,10,15];
+
+function getActiveQuestions() {
+ return QUESTIONS;
+}
+
+function blankRow(q) {
+ return {
+ id: q.id, standard: q.standard, observation: q.observation,
+ possibleMarks: q.possibleMarks ?? PRESET_MARKS[q.id - 1] ?? 10,
+ earnedScore: null, evalNote: "", screenshot: null,
+ };
+}
+
+function parseCSVLine(line) {
+ const result = []; let cur = ""; let inQ = false;
+ for (const ch of line) {
+ if (ch === '"') { inQ = !inQ; }
+ else if (ch === ',' && !inQ) { result.push(cur); cur = ""; }
+ else cur += ch;
+ }
+ result.push(cur);
+ return result;
+}
+
+// ── Helpers ───────────────────────────────────────────────────────────────────
+function fmtDuration(ms) {
+ const t = Math.max(0, Math.floor(ms / 1000));
+ const h = Math.floor(t / 3600), m = Math.floor((t % 3600) / 60), s = t % 60;
+ return `${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")}:${String(s).padStart(2,"0")}`;
+}
+
+function fmtDateTime(ts) {
+ return new Date(ts).toLocaleString("en-US", {
+ year:"numeric", month:"long", day:"numeric", hour:"2-digit", minute:"2-digit",
+ });
+}
+
+function fileToBase64(file) {
+ return new Promise((resolve) => {
+ const r = new FileReader();
+ r.onload = (e) => resolve(e.target.result);
+ r.readAsDataURL(file);
+ });
+}
+
+function dataURLtoBlob(dataURL) {
+ const [header, data] = dataURL.split(",");
+ const mime = header.match(/:(.*?);/)[1];
+ const binary = atob(data);
+ const arr = new Uint8Array(binary.length);
+ for (let i = 0; i < binary.length; i++) arr[i] = binary.charCodeAt(i);
+ return new Blob([arr], { type: mime });
+}
+
+function getMissingFields(rows) {
+ return rows.reduce((acc, r) => {
+ const f = [];
+ if (r.earnedScore === null) f.push("earned score");
+ if (!r.screenshot) f.push("screenshot");
+ if (f.length) acc.push({ id: r.id, standard: r.standard, fields: f });
+ return acc;
+ }, []);
+}
+
+// ── Report builder ────────────────────────────────────────────────────────────
+function buildReportHTML(rows, session) {
+ const { username, testingStart, testingEnd } = session;
+ const totalPossible = rows.reduce((s, r) => s + (r.possibleMarks ?? 0), 0);
+ const totalEarned = rows.reduce((s, r) => s + (r.earnedScore ?? 0), 0);
+ const pct = totalPossible > 0 ? ((totalEarned / totalPossible) * 100).toFixed(1) : "—";
+ const passed = rows.filter(r => r.possibleMarks && r.earnedScore !== null && r.earnedScore / r.possibleMarks >= 0.8).length;
+ const warned = rows.filter(r => { if (!r.possibleMarks || r.earnedScore === null) return false; const p = r.earnedScore / r.possibleMarks; return p >= 0.5 && p < 0.8; }).length;
+ const failed = rows.filter(r => r.possibleMarks && r.earnedScore !== null && r.earnedScore / r.possibleMarks < 0.5).length;
+
+ const rowsHTML = rows.map((row) => {
+ const rowPct = row.possibleMarks && row.earnedScore !== null ? Math.round((row.earnedScore / row.possibleMarks) * 100) : null;
+ const status = rowPct !== null ? (rowPct >= 80 ? "PASS" : rowPct < 50 ? "FAIL" : "WARN") : "N/A";
+ const imgSrc = row.screenshot?.base64 ?? row.screenshot?.url;
+ const imgHTML = imgSrc
+ ? ``
+ : `—`;
+ const color = CATEGORY_COLORS[row.standard.split("-")[0]] ?? "#888";
+ const noteHTML = row.evalNote?.trim()
+ ? `
| # | Standard | Observation | Possible Marks | Earned Score | Status | Screenshot |
|---|