2026-03-20 22:38:17 -05:00
<!doctype html>
< html lang = "en" class = "h-full" >
< head >
< meta charset = "UTF-8" />
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" />
2026-05-08 15:36:24 -05:00
< script src = "/assets/nav-prelude.js" ></ script >
< link rel = "modulepreload" href = "/assets/nav.js" />
< link rel = "stylesheet" href = "/vendor/inter/400.css" />
< link rel = "stylesheet" href = "/vendor/inter/600.css" />
< link rel = "stylesheet" href = "/vendor/inter/700.css" />
2026-03-20 22:38:17 -05:00
< title > Purgatory - Overlord</ title >
< link rel = "icon" type = "image/x-icon" href = "/assets/favicon.ico" />
< link rel = "stylesheet" href = "/assets/tailwind.css" />
2026-04-18 21:16:29 -05:00
< link rel = "stylesheet" href = "/vendor/fontawesome/css/all.min.css" />
2026-03-20 22:38:17 -05:00
< link rel = "stylesheet" href = "/assets/main.css" />
2026-04-01 13:49:41 -05:00
< link rel = "stylesheet" href = "/assets/custom.css" />
2026-04-08 14:37:03 -05:00
< script defer src = "/assets/toast.js" ></ script >
2026-03-20 22:38:17 -05:00
</ head >
< body class = "min-h-screen bg-slate-950 text-slate-100" >
< header id = "top-nav" ></ header >
< div class = "min-h-full flex flex-col" >
2026-06-30 13:36:27 -05:00
< main class = "flex-1 max-w-[1500px] w-full mx-auto px-4 sm:px-6 py-6" >
2026-03-20 22:38:17 -05:00
<!-- Header -->
2026-06-30 13:36:27 -05:00
< div class = "mb-5 rounded-lg border border-slate-800 bg-slate-900/60 px-5 py-4 shadow-[0_18px_55px_rgba(2,6,23,0.22)]" >
< div class = "flex flex-col xl:flex-row xl:items-center xl:justify-between gap-5" >
< div class = "flex items-center gap-3" >
< div class = "grid h-11 w-11 place-items-center rounded-lg border border-amber-400/30 bg-amber-400/10 text-amber-300" >
< i class = "fa-solid fa-user-clock" ></ i >
2026-03-20 22:38:17 -05:00
</ div >
2026-06-30 13:36:27 -05:00
< div >
< h2 class = "text-2xl font-bold text-slate-100 leading-tight" > Purgatory</ h2 >
< p class = "text-slate-400 text-sm" >
Approve or deny clients before they can connect to the server.
</ p >
</ div >
</ div >
< div class = "flex items-stretch gap-3 flex-wrap justify-start xl:justify-end" >
< div id = "stats-bar" class = "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-2 text-sm" >
< span class = "min-w-[120px] rounded-lg border border-amber-400/20 bg-amber-400/5 px-3 py-2 text-amber-300" >
< span class = "flex items-center gap-2" >
< i class = "fa-solid fa-clock text-amber-400" ></ i >
< span class = "text-lg font-semibold leading-none" id = "stat-pending" > 0</ span >
</ span >
< span class = "mt-1 block text-xs text-slate-400" > pending</ span >
</ span >
< span class = "min-w-[120px] rounded-lg border border-emerald-400/20 bg-emerald-400/5 px-3 py-2 text-emerald-300" >
< span class = "flex items-center gap-2" >
< i class = "fa-solid fa-check text-emerald-400" ></ i >
< span class = "text-lg font-semibold leading-none" id = "stat-approved" > 0</ span >
</ span >
< span class = "mt-1 block text-xs text-slate-400" > approved</ span >
</ span >
< span class = "min-w-[120px] rounded-lg border border-red-400/20 bg-red-400/5 px-3 py-2 text-red-300" >
< span class = "flex items-center gap-2" >
< i class = "fa-solid fa-ban text-red-400" ></ i >
< span class = "text-lg font-semibold leading-none" id = "stat-denied" > 0</ span >
</ span >
< span class = "mt-1 block text-xs text-slate-400" > denied</ span >
</ span >
< span class = "min-w-[120px] rounded-lg border border-orange-400/20 bg-orange-400/5 px-3 py-2 text-orange-300" >
< span class = "flex items-center gap-2" >
< i class = "fa-solid fa-triangle-exclamation text-orange-400" ></ i >
< span class = "text-lg font-semibold leading-none" id = "stat-suspicious" > 0</ span >
</ span >
< span class = "mt-1 block text-xs text-slate-400" > suspicious</ span >
</ span >
< span class = "min-w-[120px] rounded-lg border border-rose-400/20 bg-rose-400/5 px-3 py-2 text-rose-300" >
< span class = "flex items-center gap-2" >
< i class = "fa-solid fa-shield-halved text-rose-400" ></ i >
< span class = "text-lg font-semibold leading-none" id = "stat-banned-ips" > 0</ span >
</ span >
< span class = "mt-1 block text-xs text-slate-400" > banned IPs</ span >
</ span >
</ div >
< div class = "flex flex-col justify-center gap-1 border border-amber-500/30 rounded-lg px-3 py-2 bg-slate-950/50 text-sm shadow-inner shadow-amber-950/10" >
2026-05-22 10:12:40 -05:00
< div class = "flex items-center gap-2" title = "When enabled, all connecting agents are approved automatically without review" >
< i class = "fa-solid fa-door-open text-amber-400" ></ i >
< span class = "text-slate-300 font-medium" > Always Allow</ span >
< label class = "relative inline-flex items-center cursor-pointer ml-1" >
< input type = "checkbox" id = "auto-accept-toggle" class = "sr-only peer" />
< div class = "w-9 h-5 bg-slate-700 peer-checked:bg-amber-500 rounded-full transition-colors" ></ div >
< div class = "absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full transition-transform peer-checked:translate-x-4" ></ div >
</ label >
</ div >
< div id = "unless-suspicious-row" class = "hidden flex items-center gap-2 pl-1" title = "Still quarantine agents that look suspicious (HWID flood, IP flood, no hostname, etc.)" >
< i class = "fa-solid fa-shield-halved text-slate-500 text-xs" ></ i >
< span class = "text-slate-400 text-xs" > Unless suspicious</ span >
< label class = "relative inline-flex items-center cursor-pointer" >
< input type = "checkbox" id = "unless-suspicious-toggle" class = "sr-only peer" />
< div class = "w-7 h-4 bg-slate-700 peer-checked:bg-emerald-600 rounded-full transition-colors" ></ div >
< div class = "absolute left-0.5 top-0.5 w-3 h-3 bg-white rounded-full transition-transform peer-checked:translate-x-3" ></ div >
</ label >
</ div >
2026-05-22 09:32:03 -05:00
</ div >
2026-03-20 22:38:17 -05:00
</ div >
2026-06-30 13:36:27 -05:00
</ div >
2026-03-20 22:38:17 -05:00
</ div >
<!-- Filter Tabs -->
2026-06-30 13:36:27 -05:00
< div class = "mb-4 flex items-center gap-2 flex-wrap rounded-lg border border-slate-800 bg-slate-900/45 p-2" >
2026-03-20 22:38:17 -05:00
< button data-filter = "pending"
2026-06-30 13:36:27 -05:00
class = "enrollment-tab px-4 py-2 rounded-md text-sm font-medium bg-amber-500/20 text-amber-300 border border-amber-500/40" >
2026-03-20 22:38:17 -05:00
Pending
</ button >
< button data-filter = "denied"
2026-06-30 13:36:27 -05:00
class = "enrollment-tab px-4 py-2 rounded-md text-sm font-medium bg-slate-800 text-slate-400 border border-slate-700 hover:bg-slate-700" >
2026-03-20 22:38:17 -05:00
Denied
</ button >
< button data-filter = "approved"
2026-06-30 13:36:27 -05:00
class = "enrollment-tab px-4 py-2 rounded-md text-sm font-medium bg-slate-800 text-slate-400 border border-slate-700 hover:bg-slate-700" >
2026-03-20 22:38:17 -05:00
Approved
</ button >
< button data-filter = "banned-ips"
2026-06-30 13:36:27 -05:00
class = "enrollment-tab px-4 py-2 rounded-md text-sm font-medium bg-slate-800 text-slate-400 border border-slate-700 hover:bg-slate-700" >
2026-03-20 22:38:17 -05:00
< i class = "fa-solid fa-shield-halved mr-1" ></ i > Banned IPs
</ button >
2026-05-22 10:12:40 -05:00
< button data-filter = "suspicious"
2026-06-30 13:36:27 -05:00
class = "enrollment-tab px-4 py-2 rounded-md text-sm font-medium bg-slate-800 text-slate-400 border border-slate-700 hover:bg-slate-700" >
2026-05-22 10:12:40 -05:00
< i class = "fa-solid fa-triangle-exclamation mr-1" ></ i > Suspicious
</ button >
2026-03-20 22:38:17 -05:00
< div class = "flex-1" ></ div >
2026-05-22 09:32:03 -05:00
< button id = "approve-all-btn"
class = "hidden px-3 py-2 rounded-lg text-sm font-medium bg-emerald-700 hover:bg-emerald-800 text-white transition-colors" >
2026-05-22 10:12:40 -05:00
< i class = "fa-solid fa-check-double mr-1" ></ i > Approve All
</ button >
< button id = "deny-all-btn"
class = "hidden px-3 py-2 rounded-lg text-sm font-medium bg-red-700 hover:bg-red-800 text-white transition-colors" >
< i class = "fa-solid fa-ban mr-1" ></ i > Deny All
2026-05-22 09:32:03 -05:00
</ button >
2026-03-20 22:38:17 -05:00
< div class = "relative" >
< i class = "fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-slate-500 text-sm pointer-events-none" ></ i >
< input type = "text" id = "search-input" placeholder = "Search host, user, IP..."
2026-06-30 13:36:27 -05:00
class = "w-56 pl-9 pr-3 py-2 rounded-md text-sm bg-slate-950/70 border border-slate-700 text-slate-200 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent" />
2026-03-20 22:38:17 -05:00
</ div >
< button id = "bulk-approve-btn"
class = "hidden px-3 py-2 rounded-lg text-sm font-medium bg-emerald-600 hover:bg-emerald-700 text-white transition-colors" >
< i class = "fa-solid fa-check mr-1" ></ i > Approve Selected
</ button >
< button id = "bulk-deny-btn"
class = "hidden px-3 py-2 rounded-lg text-sm font-medium bg-red-600 hover:bg-red-700 text-white transition-colors" >
< i class = "fa-solid fa-ban mr-1" ></ i > Deny Selected
</ button >
< button id = "bulk-ban-btn"
class = "hidden px-3 py-2 rounded-lg text-sm font-medium bg-rose-700 hover:bg-rose-800 text-white transition-colors" >
< i class = "fa-solid fa-shield-halved mr-1" ></ i > Ban Selected IPs
</ button >
</ div >
<!-- Clients Table -->
2026-06-30 13:36:27 -05:00
< div id = "clients-table-panel" class = "bg-slate-900/55 border border-slate-800 rounded-lg overflow-x-auto shadow-[0_18px_55px_rgba(2,6,23,0.18)]" >
2026-05-30 22:44:55 -05:00
< table class = "w-full min-w-[1200px]" id = "enrollment-table" >
2026-06-30 13:36:27 -05:00
< thead class = "bg-slate-950/55 border-b border-slate-700" >
2026-05-30 22:44:55 -05:00
< tr >
< th class = "w-10 px-4 py-3" >
< input type = "checkbox" id = "select-all" class = "h-4 w-4 rounded border-slate-600" />
</ th >
< th class = "text-left px-4 py-3 text-sm font-semibold text-slate-300" > Host</ th >
< th class = "text-left px-4 py-3 text-sm font-semibold text-slate-300" > User</ th >
< th class = "text-left px-4 py-3 text-sm font-semibold text-slate-300" > OS</ th >
< th class = "text-left px-4 py-3 text-sm font-semibold text-slate-300" > CPU</ th >
< th class = "text-left px-4 py-3 text-sm font-semibold text-slate-300" > GPU</ th >
< th class = "text-left px-4 py-3 text-sm font-semibold text-slate-300" > RAM</ th >
< th class = "text-left px-4 py-3 text-sm font-semibold text-slate-300" > IP</ th >
< th class = "text-left px-4 py-3 text-sm font-semibold text-slate-300" > Country</ th >
< th class = "text-left px-4 py-3 text-sm font-semibold text-slate-300" > Key Fingerprint</ th >
< th class = "text-left px-4 py-3 text-sm font-semibold text-slate-300" > Last Seen</ th >
< th class = "text-left px-4 py-3 text-sm font-semibold text-slate-300" > Status</ th >
< th class = "text-right px-4 py-3 text-sm font-semibold text-slate-300" > Actions</ th >
</ tr >
</ thead >
< tbody id = "enrollment-body" class = "divide-y divide-slate-800" >
< tr id = "enrollment-loading" >
< td colspan = "13" class = "px-6 py-12 text-center text-slate-500" >
< i class = "fa-solid fa-spinner fa-spin mr-2" ></ i > Loading...
</ td >
</ tr >
</ tbody >
</ table >
2026-03-20 22:38:17 -05:00
< div id = "enrollment-empty" class = "hidden px-6 py-12 text-center text-slate-500" >
< i class = "fa-solid fa-inbox text-3xl mb-3 block" ></ i >
< p > No clients in this category.</ p >
</ div >
2026-07-08 23:23:18 -05:00
< div id = "enrollment-pagination" class = "hidden items-center justify-between gap-3 border-t border-slate-800 bg-slate-950/35 px-4 py-3 text-sm text-slate-400" >
< div id = "enrollment-page-summary" > Showing 0-0 of 0</ div >
< div class = "flex items-center gap-2" >
< label class = "flex items-center gap-2" >
< span > Rows</ span >
< select id = "enrollment-page-size"
class = "rounded-md border border-slate-700 bg-slate-900 px-2 py-1 text-slate-200 focus:outline-none focus:ring-2 focus:ring-sky-500" >
< option value = "50" > 50</ option >
< option value = "100" selected > 100</ option >
< option value = "200" > 200</ option >
</ select >
</ label >
< button id = "enrollment-prev-page" type = "button"
class = "rounded-md border border-slate-700 bg-slate-900 px-3 py-1.5 text-slate-200 hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-45" >
< i class = "fa-solid fa-chevron-left" ></ i >
</ button >
< span id = "enrollment-page-label" class = "min-w-[76px] text-center text-slate-300" > Page 1 / 1</ span >
< button id = "enrollment-next-page" type = "button"
class = "rounded-md border border-slate-700 bg-slate-900 px-3 py-1.5 text-slate-200 hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-45" >
< i class = "fa-solid fa-chevron-right" ></ i >
</ button >
</ div >
</ div >
2026-03-20 22:38:17 -05:00
</ div >
<!-- Banned IPs Table (hidden by default, shown on Banned IPs tab) -->
< div id = "banned-ips-section" class = "hidden" >
< div class = "mb-4 flex items-center justify-between" >
< h3 class = "text-lg font-semibold text-slate-100" >
< i class = "fa-solid fa-shield-halved text-rose-400 mr-2" ></ i > Banned IPs
</ h3 >
< button id = "add-ban-btn"
class = "px-3 py-2 rounded-lg text-sm font-medium bg-red-600 hover:bg-red-700 text-white transition-colors" >
< i class = "fa-solid fa-plus mr-1" ></ i > Ban IP
</ button >
</ div >
<!-- Manual ban form (hidden by default) -->
< div id = "manual-ban-form" class = "hidden mb-4 bg-slate-900/60 border border-slate-800 rounded-xl p-4" >
< div class = "flex items-end gap-3" >
< div class = "flex-1" >
< label class = "block text-sm font-medium text-slate-300 mb-1" > IP Address</ label >
< input type = "text" id = "ban-ip-input" placeholder = "e.g. 192.168.1.100"
class = "w-full px-3 py-2 rounded-lg text-sm bg-slate-800 border border-slate-700 text-slate-200 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent" />
</ div >
< div class = "flex-1" >
< label class = "block text-sm font-medium text-slate-300 mb-1" > Reason (optional)</ label >
< input type = "text" id = "ban-reason-input" placeholder = "Reason for ban"
class = "w-full px-3 py-2 rounded-lg text-sm bg-slate-800 border border-slate-700 text-slate-200 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent" />
</ div >
< button id = "confirm-ban-btn"
class = "px-4 py-2 rounded-lg text-sm font-medium bg-red-600 hover:bg-red-700 text-white transition-colors" >
Ban
</ button >
< button id = "cancel-ban-btn"
class = "px-4 py-2 rounded-lg text-sm font-medium bg-slate-700 hover:bg-slate-600 text-white transition-colors" >
Cancel
</ button >
</ div >
</ div >
< div class = "bg-slate-900/50 border border-slate-800 rounded-xl overflow-hidden" >
2026-05-30 22:44:55 -05:00
< table class = "w-full" >
< thead class = "bg-slate-800/50 border-b border-slate-700" >
< tr >
< th class = "text-left px-4 py-3 text-sm font-semibold text-slate-300" > IP Address</ th >
< th class = "text-left px-4 py-3 text-sm font-semibold text-slate-300" > Reason</ th >
< th class = "text-left px-4 py-3 text-sm font-semibold text-slate-300" > Banned At</ th >
< th class = "text-right px-4 py-3 text-sm font-semibold text-slate-300" > Actions</ th >
</ tr >
</ thead >
< tbody id = "banned-ips-body" class = "divide-y divide-slate-800" ></ tbody >
</ table >
2026-03-20 22:38:17 -05:00
< div id = "banned-ips-empty" class = "hidden px-6 py-12 text-center text-slate-500" >
< i class = "fa-solid fa-shield-halved text-3xl mb-3 block" ></ i >
< p > No banned IPs.</ p >
</ div >
</ div >
</ div >
</ main >
</ div >
<!-- Ban Confirmation Modal -->
< div id = "ban-confirm-modal" class = "hidden fixed inset-0 z-50 flex items-center justify-center" >
< div class = "absolute inset-0 bg-black/60 backdrop-blur-sm" id = "ban-modal-backdrop" ></ div >
< div class = "relative bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl max-w-md w-full mx-4 p-6" >
< div class = "flex items-center gap-3 mb-4" >
< div class = "flex-shrink-0 w-12 h-12 rounded-full bg-rose-500/20 flex items-center justify-center" >
< i class = "fa-solid fa-shield-halved text-rose-400 text-xl" ></ i >
</ div >
< div >
< h3 class = "text-lg font-bold text-slate-100" > Confirm IP Ban</ h3 >
< p class = "text-sm text-slate-400" > This action cannot be easily undone.</ p >
</ div >
</ div >
< div id = "ban-modal-body" class = "mb-5 text-sm text-slate-300" >
<!-- Filled dynamically -->
</ div >
< div class = "flex items-center justify-end gap-3" >
< button id = "ban-modal-cancel"
class = "px-4 py-2 rounded-lg text-sm font-medium bg-slate-700 hover:bg-slate-600 text-white transition-colors" >
Cancel
</ button >
< button id = "ban-modal-confirm"
class = "px-4 py-2 rounded-lg text-sm font-medium bg-rose-600 hover:bg-rose-700 text-white transition-colors" >
< i class = "fa-solid fa-shield-halved mr-1" ></ i > Ban
</ button >
</ div >
</ div >
</ div >
2026-05-22 10:12:40 -05:00
<!-- Deny Reason Modal -->
< div id = "deny-reason-modal" class = "hidden fixed inset-0 z-50 flex items-center justify-center" >
< div class = "absolute inset-0 bg-black/60 backdrop-blur-sm" id = "deny-reason-modal-backdrop" ></ div >
< div class = "relative bg-slate-900 border border-red-500/40 rounded-2xl shadow-2xl max-w-md w-full mx-4 p-6" >
< div class = "flex items-center gap-3 mb-4" >
< div class = "flex-shrink-0 w-12 h-12 rounded-full bg-red-500/20 flex items-center justify-center" >
< i class = "fa-solid fa-ban text-red-400 text-xl" ></ i >
</ div >
< div >
< h3 class = "text-lg font-bold text-slate-100" > Deny Client</ h3 >
< p class = "text-sm text-slate-400" > Optionally record a reason for audit purposes.</ p >
</ div >
</ div >
< div class = "mb-5" >
< label class = "block text-sm font-medium text-slate-300 mb-1" > Reason < span class = "text-slate-500 font-normal" > (optional)</ span ></ label >
< input type = "text" id = "deny-reason-input" maxlength = "200" placeholder = "Suspicious activity, unauthorized machine..."
class = "w-full px-3 py-2 rounded-lg text-sm bg-slate-800 border border-slate-700 text-slate-200 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent" />
</ div >
< div class = "flex items-center justify-end gap-3" >
< button id = "deny-reason-modal-cancel"
class = "px-4 py-2 rounded-lg text-sm font-medium bg-slate-700 hover:bg-slate-600 text-white transition-colors" >
Cancel
</ button >
< button id = "deny-reason-modal-confirm"
class = "px-4 py-2 rounded-lg text-sm font-medium bg-red-600 hover:bg-red-700 text-white transition-colors" >
< i class = "fa-solid fa-ban mr-1" ></ i > Deny
</ button >
</ div >
</ div >
</ div >
2026-05-22 09:32:03 -05:00
<!-- Always Allow Warning Modal -->
< div id = "auto-accept-modal" class = "hidden fixed inset-0 z-50 flex items-center justify-center" >
< div class = "absolute inset-0 bg-black/60 backdrop-blur-sm" id = "auto-accept-modal-backdrop" ></ div >
< div class = "relative bg-slate-900 border border-amber-500/40 rounded-2xl shadow-2xl max-w-md w-full mx-4 p-6" >
< div class = "flex items-center gap-3 mb-4" >
< div class = "flex-shrink-0 w-12 h-12 rounded-full bg-amber-500/20 flex items-center justify-center" >
< i class = "fa-solid fa-triangle-exclamation text-amber-400 text-xl" ></ i >
</ div >
< div >
< h3 class = "text-lg font-bold text-slate-100" > Disable Approval Requirement?</ h3 >
< p class = "text-sm text-amber-400" > This disables all enrollment security.</ p >
</ div >
</ div >
< div class = "mb-5 text-sm text-slate-300 space-y-2" >
< p > With < strong class = "text-amber-300" > Always Allow</ strong > enabled, < strong > any agent that connects will be automatically approved</ strong > without manual review.</ p >
< p class = "bg-amber-500/10 border border-amber-500/30 rounded-lg px-3 py-2 text-amber-200" >
< i class = "fa-solid fa-triangle-exclamation mr-1" ></ i >
This effectively disables all enrollment security. Any machine running your agent binary will gain immediate access to the server.
</ p >
</ div >
< div class = "flex items-center justify-end gap-3" >
< button id = "auto-accept-modal-cancel"
class = "px-4 py-2 rounded-lg text-sm font-medium bg-slate-700 hover:bg-slate-600 text-white transition-colors" >
Cancel
</ button >
< button id = "auto-accept-modal-confirm"
class = "px-4 py-2 rounded-lg text-sm font-medium bg-amber-600 hover:bg-amber-700 text-white transition-colors" >
< i class = "fa-solid fa-door-open mr-1" ></ i > Enable Always Allow
</ button >
</ div >
</ div >
</ div >
2026-03-20 22:38:17 -05:00
< script type = "module" src = "/assets/nav.js" ></ script >
< script type = "module" src = "/assets/purgatory-ui.js" ></ script >
</ body >
</ html >