`;
}
}
/** Report server-side deployment problems where the admin will actually see
* them: no persistent DB means everything on this page is wiped by the next
* deploy, and a per-process JWT secret means sessions die on every restart. */
function renderServerWarnings(data) {
const existing = document.getElementById("allow-storage-warning");
if (existing) existing.remove();
const problems = [];
if (data.db_persistent === false) {
problems.push("אין אחסון קבוע בשרת — משתמשים והיסטוריה יימחקו בפריסה הבאה. " +
"יש להגדיר DATABASE_URL.");
}
if (data.secret_source === "ephemeral") {
problems.push("מפתח החתימה נוצר מחדש בכל הפעלה — כל המשתמשים ינותקו בכל ריסטארט. " +
"יש להגדיר JWT_SECRET_KEY.");
}
if (problems.length === 0) return;
const el = document.createElement("p");
el.id = "allow-storage-warning";
el.className = "storage-warning";
el.textContent = "⚠ " + problems.join(" ");
allowlistBox.insertBefore(el, allowlistList);
}
/** One row per invited email, showing whether the person signed up and how
* much history they have accumulated. */
function renderAllowedEmails(invites) {
allowlistList.innerHTML = "";
if (invites.length === 0) {
allowlistList.innerHTML = "
אין משתמשים מורשים עדיין.
";
return;
}
for (const inv of invites) {
const email = inv.email;
let status;
if (!inv.registered) {
status = 'טרם נרשם';
} else {
const last = inv.last_analysis_at
? ` · אחרון: ${new Date(inv.last_analysis_at).toLocaleDateString("he-IL")}`
: "";
status = `${inv.results_count} ניתוחים${escapeHtml(last)}`;
}
const historyBtn = inv.registered
? ``
: "";
const li = document.createElement("li");
li.innerHTML = `
${escapeHtml(email)}
${inv.full_name ? `${escapeHtml(inv.full_name)}` : ""}
${status}
${historyBtn}
`;
allowlistList.appendChild(li);
}
allowlistList.querySelectorAll(".allow-remove-btn").forEach((btn) => {
btn.addEventListener("click", () => removeAllowedEmail(btn.dataset.email));
});
allowlistList.querySelectorAll(".allow-history-btn").forEach((btn) => {
btn.addEventListener("click", () =>
viewUserResults(btn.dataset.uid, btn.dataset.username));
});
}
async function addAllowedEmail() {
const email = (allowEmailInput?.value || "").trim();
const fullName = (allowNameInput?.value || "").trim();
if (!email) return;
allowMsg.textContent = "";
allowMsg.style.color = "";
try {
const resp = await fetch("/api/admin/allowed-emails", {
method: "POST",
headers: { ...getAuthHeaders(), "Content-Type": "application/json" },
body: JSON.stringify({ email, full_name: fullName || null }),
});
const data = await resp.json();
if (!resp.ok) throw new Error(data.detail || `HTTP ${resp.status}`);
allowEmailInput.value = "";
if (allowNameInput) allowNameInput.value = "";
allowMsg.style.color = "#3ddc84";
allowMsg.textContent = data.registered
? "נוסף (למשתמש כבר יש חשבון — ההיסטוריה שלו תופיע כאן)"
: "נוסף! שלחו לו את הקישור — כניסה עם האימייל והסיסמה qwertyui";
loadAllowedEmails();
} catch (err) {
allowMsg.style.color = "";
allowMsg.textContent = "שגיאה: " + err.message;
}
}
async function removeAllowedEmail(email) {
if (!confirm(`להסיר את ${email}? הגישה תיחסם, ההיסטוריה שלו תישמר.`)) return;
try {
const resp = await fetch("/api/admin/allowed-emails", {
method: "DELETE",
headers: { ...getAuthHeaders(), "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
if (!resp.ok) {
const data = await resp.json();
throw new Error(data.detail || `HTTP ${resp.status}`);
}
loadAllowedEmails();
} catch (err) {
alert("שגיאה: " + err.message);
}
}
if (allowAddBtn) {
allowAddBtn.addEventListener("click", addAllowedEmail);
}
for (const input of [allowEmailInput, allowNameInput]) {
if (!input) continue;
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") addAllowedEmail();
});
}
// ---------------------------------------------------------------------------
// Users
// ---------------------------------------------------------------------------
async function loadUsers() {
usersTbody.innerHTML = '
";
try {
const resp = await fetch(`/api/admin/users/${uid}/results`, {
headers: getAuthHeaders(),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
if (!data.results || data.results.length === 0) {
detailContent.innerHTML = "
אין ניתוחים עדיין.
";
return;
}
let html = '
';
html += "
תאריך
G
R
B
A
S
";
html += "
F0
רעידה%
HNR
";
html += "
";
for (const a of data.results) {
const ts = a.timestamp ? new Date(a.timestamp).toLocaleString() : "N/A";
const s = a.scores || {};
const f = a.features || {};
html += `