/* NetDesk admin console — manages users/roles, devices, MDM policy, API keys,
relays. Account admin-role gated (the /api/admin/* endpoints return 403 for
non-admins; the server is the authority, this UI just reacts). */
(function () {
"use strict";
var $ = function (s) { return document.querySelector(s); };
function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; }); }
function fmtTs(t) { return t ? new Date(t * 1000).toLocaleDateString() : "—"; }
function fmtAgo(t) { if (!t) return "never"; var s = Date.now() / 1000 - t;
if (s < 60) return "just now"; if (s < 3600) return Math.floor(s / 60) + "m ago";
if (s < 86400) return Math.floor(s / 3600) + "h ago"; return Math.floor(s / 86400) + "d ago"; }
var toastT;
function toast(msg, bad) { var t = $("#toast"); t.textContent = msg; t.style.borderColor = bad ? "#ef4444" : "";
t.classList.add("show"); clearTimeout(toastT); toastT = setTimeout(function () { t.classList.remove("show"); }, 2400); }
function getJSON(p) { return fetch(p, { credentials: "same-origin", cache: "no-store" }).then(function (r) {
return r.json().then(function (d) { return { ok: r.ok, status: r.status, d: d }; }).catch(function () { return { ok: r.ok, status: r.status, d: {} }; }); }); }
function post(p, body) { return fetch(p, { method: "POST", credentials: "same-origin",
headers: { "Content-Type": "application/json" }, body: JSON.stringify(body || {}) }).then(function (r) {
return r.json().then(function (d) { return { ok: r.ok, status: r.status, d: d }; }).catch(function () { return { ok: r.ok, status: r.status, d: {} }; }); }); }
/* ---- access gate ---- */
getJSON("/api/admin/overview").then(function (r) {
if (r.status === 401) { location.href = "/app"; return; }
if (r.status === 403) {
$("#gate").innerHTML = '
Admin access required Your account is signed in but is not an administrator. Ask an admin to grant you the admin role, then reload.
Back to app ';
return;
}
if (!r.ok) { $("#gate").innerHTML = 'Could not reach the server.
'; return; }
$("#gate").hidden = true; $("#shell").hidden = false;
renderOverview(r.d);
wireNav(); loadUsersForPolicy();
});
/* ---- nav ---- */
var loaded = {};
function wireNav() {
$("#nav").querySelectorAll("a").forEach(function (a) {
a.onclick = function () {
$("#nav").querySelectorAll("a").forEach(function (x) { x.classList.remove("on"); });
a.classList.add("on");
var p = a.dataset.p;
["overview", "users", "devices", "policy", "keys", "relays", "teams", "access", "recordings"].forEach(function (s) {
var el = $("#p-" + s); if (el) el.hidden = s !== p; });
if (p === "users" && !loaded.users) loadUsers();
if (p === "devices" && !loaded.devices) loadDevices();
if (p === "keys" && !loaded.keys) loadKeys();
if (p === "relays" && !loaded.relays) loadRelays();
if (p === "teams" && !loaded.teams) loadTeams();
if (p === "access" && !loaded.access) loadAccess();
if (p === "recordings" && !loaded.recordings) loadRecordings();
};
});
}
/* ---- overview ---- */
function renderOverview(d) {
var items = [["users", "Accounts"], ["admins", "Administrators"], ["devices", "Registered devices"],
["hosts_online", "Hosts online"], ["in_session", "In session now"], ["api_keys", "API keys"], ["relays", "Relay nodes"]];
$("#ovGrid").innerHTML = items.map(function (i) {
return '' + (d[i[0]] != null ? d[i[0]] : "—") + ' ' + i[1] + '
'; }).join("");
}
/* ---- users + roles + seats ---- */
var USERS = [];
function loadUsers() {
loaded.users = true;
getJSON("/api/admin/users").then(function (r) {
if (!r.ok) { toast(r.d.error || "failed", true); return; }
USERS = r.d.users || [];
$("#uCount").textContent = USERS.length + " account" + (USERS.length === 1 ? "" : "s");
$("#uBody").innerHTML = USERS.map(function (u) {
var rsel = '' +
'member ' +
'admin ';
var seat = ' ';
return '' + esc(u.email) + (u.email === (r.d.me || "") ? ' (you) ' : "") + ' ' +
'' + rsel + ' ' + seat + ' ' + (u.devices || 0) + ' ' +
'' + (u.twofa ? "on" : "off") + ' ' +
'' + (u.verified ? "yes" : "no") + ' ';
}).join("");
$("#uBody").querySelectorAll("[data-role]").forEach(function (sel) {
sel.onchange = function () { post("/api/admin/role", { email: sel.dataset.role, role: sel.value })
.then(function (x) { toast(x.ok ? "Role updated" : (x.d.error || "failed"), !x.ok); }); };
});
$("#uBody").querySelectorAll("[data-seat]").forEach(function (inp) {
inp.onchange = function () { post("/api/admin/license", { email: inp.dataset.seat, seats: parseInt(inp.value, 10) || 0 })
.then(function (x) { toast(x.ok ? "Seat limit saved" : (x.d.error || "failed"), !x.ok); }); };
});
});
}
/* ---- devices ---- */
function loadDevices() {
loaded.devices = true;
getJSON("/api/devices?scope=all").then(function (r) {
if (!r.ok) { toast(r.d.error || "failed", true); return; }
var ds = r.d.devices || [];
$("#dCount").textContent = ds.length + " device" + (ds.length === 1 ? "" : "s");
$("#dBody").innerHTML = ds.map(function (dv) {
var st = dv.online ? ' ' + (dv.busy ? "in session" : "online") : ' offline';
var tags = (dv.tags || []).map(function (t) { return '' + esc(t) + ' '; }).join("") || '— ';
return '' + esc(dv.name) + ' ' + esc(dv.id) + '
' +
'' + esc(dv.owner || "—") + ' ' + st + ' ' +
' ' +
' ' +
'Save Remove ';
}).join("") || 'No devices yet. ';
$("#dBody").querySelectorAll("tr[data-id]").forEach(function (tr) {
var id = tr.dataset.id;
var save = tr.querySelector("[data-save]"); if (save) save.onclick = function () {
post("/api/devices/meta", { id: id, group: tr.querySelector("[data-grp]").value, tags: tr.querySelector("[data-tags]").value })
.then(function (x) { toast(x.ok ? "Saved" : (x.d.error || "failed"), !x.ok); }); };
var del = tr.querySelector("[data-del]"); if (del) del.onclick = function () {
if (!confirm("Remove " + id + "? It must re-register to return.")) return;
post("/api/devices/remove", { id: id }).then(function (x) { if (x.ok) { tr.remove(); toast("Removed"); } else toast(x.d.error || "failed", true); }); };
});
});
}
/* ---- MDM policy editor ---- */
var STRKEYS = [["server", "Relay server (wss://…)"], ["reg_key", "Registration key"], ["allow_ips", "Viewer IP allowlist (CIDR, comma-sep)"], ["totp", "2FA secret (base32)"]];
function loadUsersForPolicy() {
getJSON("/api/admin/users").then(function (r) {
var sel = $("#polAcct"); if (!sel) return;
sel.innerHTML = (r.d.users || []).map(function (u) { return '' + esc(u.email) + ' '; }).join("");
renderPolicyForm({});
});
$("#polLoad").onclick = function () {
getJSON("/api/admin/policy?email=" + encodeURIComponent($("#polAcct").value)).then(function (r) { renderPolicyForm(r.d.policy || {}); toast("Loaded"); }); };
$("#polSave").onclick = function () {
post("/api/admin/policy", { email: $("#polAcct").value, policy: collectPolicy() })
.then(function (x) { toast(x.ok ? "Policy saved & will push on host registration" : (x.d.error || "failed"), !x.ok); if (x.ok) renderPolicyForm(x.d.policy || {}); }); };
$("#polClear").onclick = function () {
post("/api/admin/policy", { email: $("#polAcct").value, policy: {} }).then(function (x) { toast(x.ok ? "Policy cleared" : "failed", !x.ok); renderPolicyForm({}); }); };
}
function renderPolicyForm(pol) {
var perms = pol.perms || {};
var h = STRKEYS.map(function (k) {
return '' + k[1] + '
'; }).join("");
function chk(key, lbl, on) { return ' ' + lbl + ' '; }
h += 'Behaviour ' + chk("lock_on_end", "Lock the PC when the session ends", !!pol.lock_on_end) + chk("approve_click", "Require the host user to approve each connect", !!pol.approve_click) + chk("record_required", "Force central session recording (upload to the server)", !!pol.record_required) + '
';
h += 'Allowed channels (perms) ' +
chk("perm_input", "Keyboard / mouse input", perms.input !== false) + chk("perm_clipboard", "Clipboard sync", perms.clipboard !== false) +
chk("perm_files", "File transfer", perms.files !== false) + chk("perm_audio", "Audio", perms.audio !== false) + '
';
$("#polForm").innerHTML = h;
}
function collectPolicy() {
var pol = {}, perms = {};
$("#polForm").querySelectorAll("[data-pk]").forEach(function (i) { if (i.value.trim()) pol[i.dataset.pk] = i.value.trim(); });
$("#polForm").querySelectorAll("[data-pb]").forEach(function (c) {
var k = c.dataset.pb;
if (k.indexOf("perm_") === 0) perms[k.slice(5)] = c.checked; else if (c.checked) pol[k] = true;
});
pol.perms = perms;
return pol;
}
/* ---- API keys ---- */
function loadKeys() {
loaded.keys = true;
getJSON("/api/keys").then(function (r) {
var ks = (r.d && r.d.keys) || [];
$("#kBody").innerHTML = ks.map(function (k) {
return '' + esc(k.name) + ' ' + esc(k.key_preview) + ' ' + fmtTs(k.created) + ' ' + fmtAgo(k.last_used) + ' ' +
'Revoke '; }).join("") ||
'No API keys yet. ';
$("#kBody").querySelectorAll("[data-rev]").forEach(function (b) {
b.onclick = function () { if (!confirm("Revoke this key?")) return;
post("/api/keys/revoke", { key: b.dataset.rev }).then(function (x) { toast(x.ok ? "Revoked" : "failed", !x.ok); loaded.keys = false; loadKeys(); }); };
});
});
$("#kNew").onclick = function () {
var name = $("#kName").value.trim() || "key";
post("/api/keys", { name: name }).then(function (x) {
if (!x.ok) { toast(x.d.error || "failed", true); return; }
$("#kName").value = ""; $("#kNewBox").hidden = false;
$("#kNewBox").innerHTML = 'New key (copy it now — shown once): ' + esc(x.d.key) + '';
loaded.keys = false; loadKeys();
});
};
loadScim();
$("#scNew").onclick = function () {
var label = $("#scName").value.trim() || "SCIM";
post("/api/admin/scim/tokens", { label: label }).then(function (x) {
if (!x.ok) { toast(x.d.error || "failed", true); return; }
$("#scName").value = ""; $("#scNewBox").hidden = false;
$("#scNewBox").innerHTML = 'New SCIM token (copy now — shown once): ' + esc(x.d.token) + '';
loadScim();
});
};
}
function loadScim() {
getJSON("/api/admin/scim/tokens").then(function (r) {
if (!r.ok) return;
var ts = (r.d && r.d.tokens) || [];
if (r.d.base) $("#scBase").innerHTML = 'Point your IdP at ' + esc(r.d.base) + ' with the token as an HTTP Bearer header. Provisions accounts (Users) & teams (Groups).';
$("#scBody").innerHTML = ts.map(function (t) {
return '' + esc(t.label) + ' ' + esc(t.token_preview) + ' ' + fmtTs(t.created) + ' ' + fmtAgo(t.last_used) + ' ' +
'Revoke '; }).join("") ||
'No provisioning tokens yet. ';
$("#scBody").querySelectorAll("[data-screv]").forEach(function (b) {
b.onclick = function () { if (!confirm("Revoke this SCIM token? The IdP will stop provisioning.")) return;
post("/api/admin/scim/tokens/revoke", { token: b.dataset.screv }).then(function (x) { toast(x.ok ? "Revoked" : "failed", !x.ok); loadScim(); }); };
});
});
}
/* ---- relays ---- */
function loadRelays() {
loaded.relays = true;
getJSON("/api/relays").then(function (r) {
var rs = (r.d && r.d.relays) || [];
$("#rBody").innerHTML = rs.map(function (x) {
return '' + esc(x.id) + ' ' + esc(x.region || "—") + ' ' + esc(x.url) + ' ' + esc(x.tier || "—") + ' ' + x.lat + ", " + x.lon + ' '; }).join("");
});
}
/* ---- teams (shared address books) ---- */
var TEAMS = [], curTeam = null, ALLDEV = [];
function refreshCounts() { loaded.teams = false; loadTeams(); }
function loadTeams() {
loaded.teams = true;
getJSON("/api/admin/teams").then(function (r) {
if (!r.ok) { toast(r.d.error || "failed", true); return; }
TEAMS = r.d.teams || [];
$("#tCount").textContent = TEAMS.length + " team" + (TEAMS.length === 1 ? "" : "s");
$("#tList").innerHTML = TEAMS.map(function (t) {
return '' + esc(t.name) + ' ' +
'
' + t.members + ' member' + (t.members === 1 ? "" : "s") + ' · ' + t.devices + ' device' + (t.devices === 1 ? "" : "s") + '
' +
'
#' + t.id + ' ';
}).join("") || 'No teams yet — create one above.
';
$("#tList").querySelectorAll("[data-tid]").forEach(function (el) {
el.onclick = function () { selectTeam(parseInt(el.dataset.tid, 10)); };
});
if (curTeam && TEAMS.some(function (t) { return t.id === curTeam; })) selectTeam(curTeam);
else { curTeam = null; $("#tDetail").hidden = true; }
});
}
function selectTeam(tid) {
curTeam = tid;
var t = TEAMS.filter(function (x) { return x.id === tid; })[0];
$("#tList").querySelectorAll("[data-tid]").forEach(function (el) { el.classList.toggle("on", parseInt(el.dataset.tid, 10) === tid); });
$("#tDetail").hidden = false;
$("#tdName").textContent = t ? t.name : ("Team #" + tid);
loadMembers(tid); loadTeamDevices(tid);
}
function loadMembers(tid) {
getJSON("/api/teams/members?team_id=" + tid).then(function (r) {
var ms = (r.d && r.d.members) || [];
$("#tmList").innerHTML = ms.map(function (m) {
return '' + esc(m.email) + ' ' + esc(m.role) + ' ' +
'Remove
';
}).join("") || 'No members.
';
$("#tmList").querySelectorAll("[data-rm]").forEach(function (b) {
b.onclick = function () {
post("/api/teams/members/remove", { team_id: tid, email: b.dataset.rm }).then(function (x) {
toast(x.ok ? "Removed" : (x.d.error || "failed"), !x.ok); if (x.ok) refreshCounts(); });
};
});
});
}
function loadTeamDevices(tid) {
getJSON("/api/teams/devices?team_id=" + tid).then(function (r) {
var ds = (r.d && r.d.devices) || [], shared = {};
$("#tdDevList").innerHTML = ds.map(function (d) {
shared[d.id] = true;
return '' + (d.online ? ' ' : ' ') + esc(d.name) +
' ' + esc(d.owner || "") + ' Unshare
';
}).join("") || 'No shared devices.
';
$("#tdDevList").querySelectorAll("[data-unshare]").forEach(function (b) {
b.onclick = function () {
post("/api/teams/devices/remove", { team_id: tid, device_id: b.dataset.unshare }).then(function (x) {
toast(x.ok ? "Unshared" : (x.d.error || "failed"), !x.ok); if (x.ok) refreshCounts(); });
};
});
var fill = function () {
$("#tdDev").innerHTML = ALLDEV.filter(function (d) { return !shared[d.id]; }).map(function (d) {
return '' + esc(d.name) + ' (' + esc(d.id) + (d.owner ? " · " + esc(d.owner) : "") + ') ';
}).join("") || '— no more devices — ';
};
if (ALLDEV.length) fill();
else getJSON("/api/devices?scope=all").then(function (rr) { ALLDEV = (rr.d && rr.d.devices) || []; fill(); });
});
}
(function wireTeams() {
var nw = $("#tNew"); if (nw) nw.onclick = function () {
var name = $("#tName").value.trim(); if (!name) { toast("Enter a team name", true); return; }
post("/api/teams", { name: name }).then(function (x) {
if (!x.ok) { toast(x.d.error || "failed", true); return; }
$("#tName").value = ""; toast("Team created"); curTeam = x.d.id; refreshCounts(); });
};
var ad = $("#tmAdd"); if (ad) ad.onclick = function () {
if (!curTeam) return; var em = $("#tmEmail").value.trim(); if (!em) { toast("Enter an account email", true); return; }
post("/api/teams/members", { team_id: curTeam, email: em, role: $("#tmRole").value }).then(function (x) {
toast(x.ok ? "Member added" : (x.d.error || "failed"), !x.ok); if (x.ok) { $("#tmEmail").value = ""; refreshCounts(); } });
};
var sh = $("#tdShare"); if (sh) sh.onclick = function () {
if (!curTeam) return; var id = $("#tdDev").value; if (!id) { toast("No device selected", true); return; }
post("/api/teams/devices", { team_id: curTeam, device_id: id }).then(function (x) {
toast(x.ok ? "Device shared" : (x.d.error || "failed"), !x.ok); if (x.ok) refreshCounts(); });
};
var rn = $("#tdRename"); if (rn) rn.onclick = function () {
if (!curTeam) return;
var cur = (TEAMS.filter(function (x) { return x.id === curTeam; })[0] || {}).name || "";
var name = prompt("Rename team", cur); if (name == null || !name.trim()) return;
post("/api/teams/rename", { team_id: curTeam, name: name.trim() }).then(function (x) {
toast(x.ok ? "Renamed" : (x.d.error || "failed"), !x.ok); if (x.ok) refreshCounts(); });
};
var dl = $("#tdDelete"); if (dl) dl.onclick = function () {
if (!curTeam) return;
if (!confirm("Delete this team? Members lose the shared list (the devices themselves are not deleted).")) return;
post("/api/teams/delete", { team_id: curTeam }).then(function (x) {
toast(x.ok ? "Team deleted" : (x.d.error || "failed"), !x.ok);
if (x.ok) { curTeam = null; $("#tDetail").hidden = true; refreshCounts(); } });
};
})();
/* ---- session recordings ---- */
function fmtSize(b) { b = +b || 0; if (b < 1024) return b + " B"; if (b < 1048576) return (b / 1024).toFixed(1) + " KB"; if (b < 1073741824) return (b / 1048576).toFixed(1) + " MB"; return (b / 1073741824).toFixed(2) + " GB"; }
function fmtDur(s) { s = +s || 0; if (!s) return "—"; var m = Math.floor(s / 60), ss = s % 60; return m + ":" + ("" + ss).padStart(2, "0"); }
function loadRecordings() {
loaded.recordings = true;
getJSON("/api/recordings").then(function (r) {
if (!r.ok) { toast(r.d.error || "failed", true); return; }
var rs = r.d.recordings || [];
$("#recRetention").textContent = r.d.retention_days > 0 ? ("Auto-deleted after " + r.d.retention_days + " days.") : "Kept indefinitely (no retention limit set).";
$("#recCount").textContent = rs.length + " recording" + (rs.length === 1 ? "" : "s");
$("#recBody").innerHTML = rs.map(function (x) {
return '' + x.id + ' ' + esc(x.host_id) + ' ' + esc(x.owner || "—") + ' ' +
'' + esc(x.viewer || "—") + ' ' + fmtSize(x.size) + ' ' + fmtDur(x.duration) + ' ' +
'' + fmtTs(x.created) + ' ' +
'Download Delete ';
}).join("") || 'No recordings yet. Set a record_required MDM policy on an account, then its hosts upload session recordings here. ';
$("#recBody").querySelectorAll("[data-recdel]").forEach(function (b) {
b.onclick = function () { if (!confirm("Delete this recording permanently?")) return;
post("/api/recordings/delete", { id: +b.dataset.recdel }).then(function (x) { toast(x.ok ? "Deleted" : (x.d.error || "failed"), !x.ok); loadRecordings(); }); };
});
});
}
/* ---- conditional access rules ---- */
var DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
var editRuleId = null;
function minsToHHMM(m) { if (m == null || m === "") return ""; m = +m; return (("" + Math.floor(m / 60)).padStart(2, "0")) + ":" + (("" + (m % 60)).padStart(2, "0")); }
function hhmmToMins(s) { s = (s || "").trim(); if (!s) return null; var p = s.split(":"); if (p.length !== 2) return null; var h = +p[0], m = +p[1]; if (isNaN(h) || isNaN(m)) return null; return h * 60 + m; }
function acOwner() { var s = $("#acAcct"); return (s && s.value) || ""; }
function loadAccess() {
loaded.access = true;
// day checkboxes
$("#acDays").innerHTML = DAYS.map(function (d, i) { return ' ' + d + ' '; }).join("");
// account selector (reuse users list)
getJSON("/api/admin/users").then(function (r) {
var us = (r.d && r.d.users) || [], me = (r.d && r.d.me) || "";
$("#acAcct").innerHTML = us.map(function (u) { return '' + esc(u.email) + ' '; }).join("");
loadRules();
});
var lb = $("#acLoad"); if (lb) lb.onclick = loadRules;
}
function condText(r) {
var bits = [];
if (r.cidr) bits.push("from " + esc(r.cidr));
if (r.weekdays) bits.push(r.weekdays.split(",").map(function (i) { return DAYS[+i] || i; }).join(" "));
if (r.time_start != null && r.time_end != null) bits.push(minsToHHMM(r.time_start) + "–" + minsToHHMM(r.time_end) + (r.tz_offset ? " (UTC" + (r.tz_offset >= 0 ? "+" : "") + (r.tz_offset / 60) + ")" : " UTC"));
return bits.join(" · ") || 'any ';
}
function subjText(r) {
if (r.subject_type === "any") return 'any connection ';
return esc(r.subject_type) + ": " + esc(r.subject || "—") + " ";
}
function loadRules() {
var owner = acOwner();
getJSON("/api/access/rules?owner=" + encodeURIComponent(owner)).then(function (r) {
if (!r.ok) { toast(r.d.error || "failed", true); return; }
var rs = r.d.rules || [];
$("#acBody").innerHTML = rs.map(function (x) {
return '' + x.priority + ' ' +
'' + x.action + ' ' +
'' + subjText(x) + ' ' + condText(x) + ' ' +
'' + esc(x.reason || "") + ' ' +
' ' +
'Edit Delete ';
}).join("") || 'No rules — every connection is allowed. Add a rule below. ';
$("#acBody").querySelectorAll("[data-del]").forEach(function (b) {
b.onclick = function () { if (!confirm("Delete this rule?")) return;
post("/api/access/rules/delete", { id: +b.dataset.del }).then(function (x) { toast(x.ok ? "Deleted" : "failed", !x.ok); loadRules(); }); };
});
$("#acBody").querySelectorAll("[data-toggle]").forEach(function (cb) {
cb.onchange = function () { var rule = rs.filter(function (z) { return z.id === +cb.dataset.toggle; })[0]; if (!rule) return;
post("/api/access/rules", ruleBody(rule, { enabled: cb.checked })).then(function (x) { toast(x.ok ? (cb.checked ? "Enabled" : "Disabled") : "failed", !x.ok); }); };
});
$("#acBody").querySelectorAll("[data-edit]").forEach(function (b) {
b.onclick = function () { var rule = rs.filter(function (z) { return z.id === +b.dataset.edit; })[0]; if (rule) fillRuleForm(rule); };
});
});
}
function ruleBody(base, over) {
var b = { owner: acOwner(), id: base.id, priority: base.priority, action: base.action, subject_type: base.subject_type,
subject: base.subject, cidr: base.cidr, weekdays: base.weekdays, time_start: base.time_start, time_end: base.time_end,
tz_offset: base.tz_offset, reason: base.reason, enabled: base.enabled };
for (var k in (over || {})) b[k] = over[k];
return b;
}
function fillRuleForm(r) {
editRuleId = r.id;
$("#acFormTitle").textContent = "Edit rule #" + r.id;
$("#acReset").hidden = false;
$("#acPrio").value = r.priority; $("#acAction").value = r.action; $("#acSubType").value = r.subject_type;
$("#acSubject").value = r.subject || ""; $("#acCidr").value = r.cidr || ""; $("#acReason").value = r.reason || "";
$("#acTimeStart").value = minsToHHMM(r.time_start); $("#acTimeEnd").value = minsToHHMM(r.time_end); $("#acTz").value = r.tz_offset || 0;
var days = (r.weekdays || "").split(",");
$("#acDays").querySelectorAll("[data-day]").forEach(function (cb) { cb.checked = days.indexOf(cb.dataset.day) >= 0; });
try { $("#acFormTitle").scrollIntoView({ behavior: "smooth", block: "center" }); } catch (e) {}
}
function resetRuleForm() {
editRuleId = null; $("#acFormTitle").textContent = "Add a rule"; $("#acReset").hidden = true;
$("#acPrio").value = 100; $("#acAction").value = "deny"; $("#acSubType").value = "any";
$("#acSubject").value = ""; $("#acCidr").value = ""; $("#acReason").value = ""; $("#acTimeStart").value = ""; $("#acTimeEnd").value = ""; $("#acTz").value = 0;
$("#acDays").querySelectorAll("[data-day]").forEach(function (cb) { cb.checked = false; });
}
(function wireAccess() {
var sv = $("#acSave"); if (sv) sv.onclick = function () {
var days = [];
$("#acDays").querySelectorAll("[data-day]").forEach(function (cb) { if (cb.checked) days.push(cb.dataset.day); });
var body = { owner: acOwner(), priority: +$("#acPrio").value || 100, action: $("#acAction").value,
subject_type: $("#acSubType").value, subject: $("#acSubject").value.trim(), cidr: $("#acCidr").value.trim(),
reason: $("#acReason").value.trim(), weekdays: days.join(","), tz_offset: +$("#acTz").value || 0,
time_start: hhmmToMins($("#acTimeStart").value), time_end: hhmmToMins($("#acTimeEnd").value), enabled: true };
if (editRuleId) body.id = editRuleId;
post("/api/access/rules", body).then(function (x) {
toast(x.ok ? "Rule saved" : (x.d.error || "failed"), !x.ok);
if (x.ok) { resetRuleForm(); loadRules(); } });
};
var rs = $("#acReset"); if (rs) rs.onclick = resetRuleForm;
var tr = $("#tRun"); if (tr) tr.onclick = function () {
var q = new URLSearchParams({ owner: acOwner(), viewer: $("#tViewer").value.trim(), host: $("#tHost").value.trim(), group: $("#tGroup").value.trim(), ip: $("#tIp").value.trim() });
getJSON("/api/access/test?" + q.toString()).then(function (x) {
if (!x.ok) { $("#tResult").innerHTML = '' + esc(x.d.error || "error") + ' '; return; }
var d = x.d;
$("#tResult").innerHTML = d.allowed
? '✓ ALLOWED ' + (d.rule_id ? ' by rule #' + d.rule_id + ' ' : ' (no rule matched — default allow) ')
: '✕ DENIED rule #' + esc(d.rule_id) + ' — ' + esc(d.reason) + ' ';
});
};
})();
})();