1013 lines
30 KiB
HTML
Executable file
1013 lines
30 KiB
HTML
Executable file
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Admin Panel — deRenzy BT</title>
|
|
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
|
|
<link rel="icon" href="/assets/favicon-192.png" type="image/png" sizes="192x192">
|
|
<link rel="apple-touch-icon" href="/assets/apple-touch-icon.png">
|
|
<link rel="stylesheet" href="/styles/variables.css">
|
|
<link rel="stylesheet" href="/styles/main.css">
|
|
<style>
|
|
/* ── Layout ── */
|
|
/* Ensure hidden attribute is respected even when element has display:flex/block via class */
|
|
[hidden] { display: none !important; }
|
|
|
|
body { min-height: 100vh; }
|
|
|
|
#admin-main {
|
|
flex: 1;
|
|
padding: 32px 24px;
|
|
max-width: 1100px;
|
|
width: 100%;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* ── Page title ── */
|
|
.page-title {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.page-title h1 {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
color: var(--gray-900);
|
|
}
|
|
|
|
.page-title p {
|
|
font-size: 0.85rem;
|
|
color: var(--gray-500);
|
|
margin-top: 3px;
|
|
}
|
|
|
|
/* ── Section card ── */
|
|
.admin-section {
|
|
background: var(--white);
|
|
border: 1px solid var(--gray-200);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-sm);
|
|
margin-bottom: 28px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 18px 24px;
|
|
border-bottom: 1px solid var(--gray-200);
|
|
}
|
|
|
|
.section-header h2 {
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
color: var(--gray-800);
|
|
}
|
|
|
|
.section-header p {
|
|
font-size: 0.8rem;
|
|
color: var(--gray-500);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
/* ── Filter bar ── */
|
|
.filter-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 12px 24px;
|
|
border-bottom: 1px solid var(--gray-200);
|
|
background: var(--gray-50);
|
|
}
|
|
|
|
.filter-bar label {
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
color: var(--gray-600);
|
|
text-transform: uppercase;
|
|
letter-spacing: .04em;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
#company-filter {
|
|
border: 1.5px solid var(--gray-300);
|
|
border-radius: var(--radius-sm);
|
|
padding: 5px 10px;
|
|
font-size: 0.85rem;
|
|
font-family: var(--font);
|
|
color: var(--gray-800);
|
|
background: var(--white);
|
|
outline: none;
|
|
transition: border-color .15s;
|
|
min-width: 180px;
|
|
}
|
|
|
|
#company-filter:focus { border-color: var(--primary); }
|
|
|
|
/* ── Company group header row ── */
|
|
.group-row td {
|
|
background: var(--gray-100);
|
|
color: var(--gray-600);
|
|
font-size: 0.72rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: .06em;
|
|
padding: 7px 16px;
|
|
border-bottom: 1px solid var(--gray-200);
|
|
}
|
|
|
|
/* ── User table ── */
|
|
.user-table-wrap {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 0.88rem;
|
|
}
|
|
|
|
thead th {
|
|
background: var(--gray-50);
|
|
color: var(--gray-600);
|
|
font-size: 0.72rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: .05em;
|
|
padding: 10px 16px;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--gray-200);
|
|
}
|
|
|
|
tbody tr {
|
|
border-bottom: 1px solid var(--gray-100);
|
|
transition: background .1s;
|
|
}
|
|
|
|
tbody tr:last-child { border-bottom: none; }
|
|
tbody tr:hover { background: var(--gray-50); }
|
|
|
|
td {
|
|
padding: 13px 16px;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.td-name { font-weight: 600; color: var(--gray-900); }
|
|
.td-username { color: var(--gray-600); font-family: monospace; font-size: 0.85rem; }
|
|
|
|
.td-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
|
|
/* ── Role badge ── */
|
|
.role-badge {
|
|
display: inline-block;
|
|
font-size: 0.68rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: .04em;
|
|
padding: 2px 8px;
|
|
border-radius: 99px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.role-badge[data-role="superduperadmin"] { background: var(--accent-50); color: var(--accent-dark); }
|
|
.role-badge[data-role="admin"] { background: var(--primary-50); color: var(--primary-dark); }
|
|
.role-badge[data-role="tech"] { background: var(--gray-100); color: var(--gray-600); }
|
|
.role-badge[data-role="client"] { background: var(--purple-bg); color: var(--purple); }
|
|
|
|
/* ── Status badge ── */
|
|
.status-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
padding: 2px 8px;
|
|
border-radius: 99px;
|
|
}
|
|
|
|
.status-badge.active { background: var(--green-bg); color: var(--green); }
|
|
.status-badge.inactive { background: var(--gray-100); color: var(--gray-500); }
|
|
|
|
.status-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.status-badge.active .status-dot { background: var(--green); }
|
|
.status-badge.inactive .status-dot { background: var(--gray-400); }
|
|
|
|
/* ── Server section ── */
|
|
.server-body {
|
|
padding: 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 24px;
|
|
}
|
|
|
|
.server-info p {
|
|
font-size: 0.88rem;
|
|
color: var(--gray-600);
|
|
max-width: 480px;
|
|
line-height: 1.55;
|
|
}
|
|
|
|
.btn-restart {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
background: var(--yellow-bg);
|
|
color: var(--yellow);
|
|
border: 1px solid #fde68a;
|
|
border-radius: var(--radius-sm);
|
|
cursor: pointer;
|
|
font-size: 0.88rem;
|
|
font-family: var(--font);
|
|
font-weight: 700;
|
|
padding: 10px 20px;
|
|
white-space: nowrap;
|
|
transition: background .15s, border-color .15s;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.btn-restart:hover { background: #fef08a; border-color: #fbbf24; }
|
|
.btn-restart:disabled { opacity: .6; cursor: not-allowed; }
|
|
|
|
.btn-restart svg { width: 16px; height: 16px; flex-shrink: 0; }
|
|
|
|
/* ── Empty state ── */
|
|
.empty-state {
|
|
padding: 48px 24px;
|
|
text-align: center;
|
|
color: var(--gray-400);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
/* ── Modal backdrop ── */
|
|
.modal-backdrop {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0,0,0,.45);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 24px;
|
|
z-index: 200;
|
|
animation: fadeBackdrop .15s ease;
|
|
}
|
|
|
|
@keyframes fadeBackdrop {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
.modal {
|
|
background: var(--white);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-lg);
|
|
width: 100%;
|
|
max-width: 480px;
|
|
animation: slideModal .2s ease;
|
|
overflow: hidden;
|
|
}
|
|
|
|
@keyframes slideModal {
|
|
from { transform: translateY(-16px); opacity: 0; }
|
|
to { transform: translateY(0); opacity: 1; }
|
|
}
|
|
|
|
.modal-header {
|
|
background: var(--primary-dark);
|
|
color: var(--white);
|
|
padding: 18px 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
border-bottom: 3px solid var(--accent);
|
|
}
|
|
|
|
.modal-header h3 {
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.modal-close {
|
|
background: none;
|
|
border: none;
|
|
color: rgba(255,255,255,.6);
|
|
cursor: pointer;
|
|
font-size: 1.2rem;
|
|
line-height: 1;
|
|
padding: 2px 6px;
|
|
border-radius: var(--radius-sm);
|
|
transition: color .15s, background .15s;
|
|
}
|
|
|
|
.modal-close:hover { color: var(--white); background: rgba(255,255,255,.1); }
|
|
|
|
.modal-body {
|
|
padding: 24px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
.modal-error {
|
|
background: var(--red-bg);
|
|
border: 1px solid #fca5a5;
|
|
border-radius: var(--radius-sm);
|
|
color: var(--red);
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
padding: 10px 14px;
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.form-group label {
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
color: var(--gray-700);
|
|
text-transform: uppercase;
|
|
letter-spacing: .04em;
|
|
}
|
|
|
|
.form-group input,
|
|
.form-group select {
|
|
border: 1.5px solid var(--gray-300);
|
|
border-radius: var(--radius-sm);
|
|
padding: 9px 12px;
|
|
font-size: 0.92rem;
|
|
font-family: var(--font);
|
|
color: var(--gray-900);
|
|
outline: none;
|
|
transition: border-color .15s, box-shadow .15s;
|
|
}
|
|
|
|
.form-group input:focus,
|
|
.form-group select:focus {
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px rgba(43,84,153,.12);
|
|
}
|
|
|
|
.form-hint {
|
|
font-size: 0.75rem;
|
|
color: var(--gray-500);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.modal-footer {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 10px;
|
|
padding: 16px 24px;
|
|
border-top: 1px solid var(--gray-100);
|
|
}
|
|
|
|
/* ── Confirm dialog ── */
|
|
.confirm-modal { max-width: 400px; }
|
|
|
|
.confirm-body {
|
|
padding: 24px;
|
|
}
|
|
|
|
.confirm-body p {
|
|
font-size: 0.92rem;
|
|
color: var(--gray-700);
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.confirm-body strong { color: var(--gray-900); }
|
|
|
|
/* ── Small btn variants ── */
|
|
.btn-sm {
|
|
padding: 6px 12px;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
/* ── Syncro customer picker ── */
|
|
.customer-picker {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.customer-search-input {
|
|
border: 1.5px solid var(--gray-300);
|
|
border-bottom: none;
|
|
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
|
|
padding: 8px 12px;
|
|
font-size: 0.92rem;
|
|
font-family: var(--font);
|
|
color: var(--gray-900);
|
|
outline: none;
|
|
transition: border-color .15s, box-shadow .15s;
|
|
}
|
|
|
|
.customer-search-input:focus {
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px rgba(43,84,153,.12);
|
|
z-index: 1;
|
|
}
|
|
|
|
.customer-listbox {
|
|
border: 1.5px solid var(--gray-300);
|
|
border-radius: 0 0 var(--radius-sm) var(--radius-sm);
|
|
font-size: 0.88rem;
|
|
font-family: var(--font);
|
|
width: 100%;
|
|
outline: none;
|
|
background: var(--white);
|
|
transition: border-color .15s;
|
|
}
|
|
|
|
.customer-listbox:focus { border-color: var(--primary); }
|
|
|
|
.customer-listbox option {
|
|
padding: 6px 10px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.customer-search-input:focus + .customer-listbox,
|
|
.customer-listbox:focus {
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
/* ── API usage bar ── */
|
|
.api-usage-body {
|
|
padding: 20px 24px;
|
|
}
|
|
|
|
.api-usage-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 24px;
|
|
}
|
|
|
|
.api-usage-bar-wrap {
|
|
flex: 1;
|
|
background: var(--gray-100);
|
|
border-radius: 99px;
|
|
height: 10px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.api-usage-bar {
|
|
height: 100%;
|
|
border-radius: 99px;
|
|
background: var(--green);
|
|
transition: width .4s ease, background-color .4s ease;
|
|
width: 0%;
|
|
}
|
|
|
|
.api-usage-bar.warn { background: var(--yellow); }
|
|
.api-usage-bar.crit { background: var(--red); }
|
|
|
|
.api-usage-label {
|
|
font-size: 0.85rem;
|
|
font-weight: 700;
|
|
color: var(--gray-700);
|
|
white-space: nowrap;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.api-usage-sub {
|
|
font-size: 0.75rem;
|
|
color: var(--gray-400);
|
|
margin-top: 6px;
|
|
}
|
|
|
|
/* ── Usage history panel ── */
|
|
.usage-history-panel {
|
|
padding: 14px 24px 16px;
|
|
border-bottom: 1px solid var(--gray-200);
|
|
background: var(--gray-50);
|
|
}
|
|
.usage-history-panel[hidden] { display: none; }
|
|
|
|
.usage-tf-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.usage-tf-label {
|
|
font-size: 0.68rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: .06em;
|
|
color: var(--gray-400);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.usage-tf-chip {
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
padding: 3px 10px;
|
|
border-radius: 4px;
|
|
border: 1px solid var(--gray-300);
|
|
background: var(--white);
|
|
color: var(--gray-600);
|
|
cursor: pointer;
|
|
font-family: var(--font);
|
|
line-height: 1.5;
|
|
transition: background .1s, color .1s, border-color .1s;
|
|
}
|
|
.usage-tf-chip:hover { border-color: var(--primary-light); color: var(--primary); background: var(--primary-50); }
|
|
.usage-tf-chip.active { background: var(--primary); border-color: var(--primary); color: var(--white); }
|
|
|
|
.usage-chart-wrap {
|
|
width: 100%;
|
|
height: 130px;
|
|
position: relative;
|
|
}
|
|
|
|
#usage-chart {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
/* ── Header admin indicator ── */
|
|
.header-admin-badge {
|
|
font-size: 0.68rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: .06em;
|
|
background: var(--accent);
|
|
color: var(--white);
|
|
padding: 2px 8px;
|
|
border-radius: 99px;
|
|
}
|
|
|
|
/* ── Admin user filter panel ── */
|
|
.admin-filter-panel {
|
|
padding: 12px 24px;
|
|
border-bottom: 1px solid var(--gray-200);
|
|
background: var(--gray-50);
|
|
display: flex;
|
|
align-items: flex-start;
|
|
flex-wrap: wrap;
|
|
gap: 16px;
|
|
}
|
|
|
|
.admin-filter-panel[hidden] { display: none; }
|
|
|
|
.af-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
}
|
|
|
|
.af-label {
|
|
font-size: 0.65rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: .07em;
|
|
color: var(--gray-400);
|
|
}
|
|
|
|
.af-chips {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px;
|
|
}
|
|
|
|
.af-chip {
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
padding: 3px 10px;
|
|
border-radius: 4px;
|
|
border: 1px solid var(--gray-300);
|
|
background: var(--white);
|
|
color: var(--gray-600);
|
|
cursor: pointer;
|
|
transition: background .1s, color .1s, border-color .1s;
|
|
font-family: var(--font);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.af-chip:hover { border-color: var(--primary-light); color: var(--primary); background: var(--primary-50); }
|
|
.af-chip.active { background: var(--primary); border-color: var(--primary); color: var(--white); }
|
|
|
|
[data-filter-group="role"] .af-chip[data-value="superduperadmin"].active { background: var(--accent-dark); border-color: var(--accent-dark); }
|
|
[data-filter-group="role"] .af-chip[data-value="admin"].active { background: var(--primary-dark); border-color: var(--primary-dark); }
|
|
[data-filter-group="role"] .af-chip[data-value="tech"].active { background: var(--gray-600); border-color: var(--gray-600); }
|
|
[data-filter-group="role"] .af-chip[data-value="client"].active { background: var(--purple); border-color: var(--purple); }
|
|
|
|
[data-filter-group="status"] .af-chip[data-value="active"].active { background: var(--green); border-color: var(--green); }
|
|
[data-filter-group="status"] .af-chip[data-value="inactive"].active { background: var(--gray-500); border-color: var(--gray-500); }
|
|
|
|
.af-separator { width: 1px; background: var(--gray-200); align-self: stretch; margin: 0 4px; }
|
|
|
|
.af-clear {
|
|
font-size: 0.72rem;
|
|
font-weight: 600;
|
|
color: var(--gray-400);
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
font-family: var(--font);
|
|
padding: 3px 6px;
|
|
border-radius: var(--radius-sm);
|
|
transition: color .1s, background .1s;
|
|
align-self: flex-end;
|
|
margin-left: auto;
|
|
}
|
|
.af-clear:hover { color: var(--red); background: var(--red-bg); }
|
|
|
|
#company-filter {
|
|
border: 1.5px solid var(--gray-300);
|
|
border-radius: var(--radius-sm);
|
|
padding: 4px 10px;
|
|
font-size: 0.8rem;
|
|
font-family: var(--font);
|
|
color: var(--gray-800);
|
|
background: var(--white);
|
|
outline: none;
|
|
transition: border-color .15s;
|
|
min-width: 160px;
|
|
}
|
|
#company-filter:focus { border-color: var(--primary); }
|
|
|
|
/* filter icon + badge wrapper */
|
|
.filter-icon-wrap {
|
|
position: relative;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.btn-filter-badge {
|
|
position: absolute;
|
|
top: -5px;
|
|
right: -6px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--accent);
|
|
color: var(--white);
|
|
font-size: 0.58rem;
|
|
font-weight: 700;
|
|
width: 14px;
|
|
height: 14px;
|
|
border-radius: 50%;
|
|
pointer-events: none;
|
|
border: 1.5px solid var(--white);
|
|
}
|
|
|
|
/* history button limit-hit badge */
|
|
.history-icon-wrap {
|
|
position: relative;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.btn-history-badge {
|
|
position: absolute;
|
|
top: -5px;
|
|
right: -6px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--red);
|
|
color: var(--white);
|
|
font-size: 0.58rem;
|
|
font-weight: 700;
|
|
min-width: 14px;
|
|
height: 14px;
|
|
border-radius: 99px;
|
|
padding: 0 3px;
|
|
pointer-events: none;
|
|
border: 1.5px solid var(--white);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- ── Header ── -->
|
|
<header id="app-header">
|
|
<div class="header-left">
|
|
<img src="/assets/logo-swirl.png" alt="deRenzy BT" class="header-logo">
|
|
<div class="header-title">
|
|
<span class="title-main">Admin Panel</span>
|
|
<span class="title-sub">deRenzy Business Technologies</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="header-center"></div>
|
|
|
|
<div class="header-right">
|
|
<nav class="mode-nav" aria-label="Navigation">
|
|
<a href="/" class="mode-btn" style="text-decoration:none;">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
<polyline points="15 18 9 12 15 6"/>
|
|
</svg>
|
|
Back
|
|
</a>
|
|
</nav>
|
|
|
|
<span class="header-btn-divider" aria-hidden="true"></span>
|
|
|
|
<div class="app-menu-wrap">
|
|
<button class="mode-btn" id="btn-menu-toggle" aria-haspopup="true" aria-expanded="false">
|
|
Menu
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
<line x1="3" y1="6" x2="21" y2="6"/>
|
|
<line x1="3" y1="12" x2="21" y2="12"/>
|
|
<line x1="3" y1="18" x2="21" y2="18"/>
|
|
</svg>
|
|
</button>
|
|
|
|
<div id="app-menu" class="app-menu" hidden>
|
|
<div class="app-menu-user">
|
|
<span class="user-name" id="user-name">…</span>
|
|
<span class="user-role-badge" id="user-role"></span>
|
|
</div>
|
|
<div class="app-menu-divider"></div>
|
|
<button class="app-menu-item app-menu-item-danger" id="btn-logout">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/>
|
|
<polyline points="16 17 21 12 16 7"/>
|
|
<line x1="21" y1="12" x2="9" y2="12"/>
|
|
</svg>
|
|
Sign Out
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- ── Main ── -->
|
|
<main id="admin-main">
|
|
|
|
<!-- Page title -->
|
|
<div class="page-title">
|
|
<div>
|
|
<h1>Admin Panel</h1>
|
|
<p>Manage users and server settings</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Users section -->
|
|
<section class="admin-section" id="section-users">
|
|
<div class="section-header">
|
|
<div>
|
|
<h2>Users</h2>
|
|
<p>Manage user accounts and roles</p>
|
|
</div>
|
|
<div style="display:flex;gap:8px;align-items:center;">
|
|
<button class="btn btn-ghost btn-sm" id="btn-user-filter">
|
|
<span class="filter-icon-wrap">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/>
|
|
</svg>
|
|
<span class="btn-filter-badge" id="user-filter-badge" hidden>0</span>
|
|
</span>
|
|
Filter
|
|
</button>
|
|
<button class="btn btn-primary btn-sm" id="btn-new-user">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
|
</svg>
|
|
New User
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filter panel -->
|
|
<div id="user-filter-panel" class="admin-filter-panel" hidden>
|
|
<div class="af-group">
|
|
<div class="af-label">Company</div>
|
|
<select id="company-filter">
|
|
<option value="">All Companies</option>
|
|
</select>
|
|
</div>
|
|
<div class="af-separator" aria-hidden="true"></div>
|
|
<div class="af-group" data-filter-group="role">
|
|
<div class="af-label">Role</div>
|
|
<div class="af-chips">
|
|
<button class="af-chip" data-value="client">Client</button>
|
|
<button class="af-chip" data-value="tech">Tech</button>
|
|
<button class="af-chip" data-value="admin">Admin</button>
|
|
<button class="af-chip" data-value="superduperadmin">Super Admin</button>
|
|
</div>
|
|
</div>
|
|
<div class="af-separator" aria-hidden="true"></div>
|
|
<div class="af-group" data-filter-group="status">
|
|
<div class="af-label">Status</div>
|
|
<div class="af-chips">
|
|
<button class="af-chip active" data-value="">All</button>
|
|
<button class="af-chip" data-value="active">Active</button>
|
|
<button class="af-chip" data-value="inactive">Inactive</button>
|
|
</div>
|
|
</div>
|
|
<div class="af-separator" aria-hidden="true"></div>
|
|
<div class="af-group" data-filter-group="groupby">
|
|
<div class="af-label">Group By</div>
|
|
<div class="af-chips">
|
|
<button class="af-chip active" data-value="company">Company</button>
|
|
<button class="af-chip" data-value="role">Role</button>
|
|
<button class="af-chip" data-value="status">Status</button>
|
|
<button class="af-chip" data-value="">None</button>
|
|
</div>
|
|
</div>
|
|
<button id="af-clear" class="af-clear">Clear Filters</button>
|
|
</div>
|
|
<div class="user-table-wrap">
|
|
<table id="user-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Username</th>
|
|
<th>Company</th>
|
|
<th>Role</th>
|
|
<th>Status</th>
|
|
<th>Created</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="user-tbody">
|
|
<tr><td colspan="7" class="empty-state">Loading…</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- API Usage section -->
|
|
<section class="admin-section" id="section-api-usage">
|
|
<div class="section-header">
|
|
<div>
|
|
<h2>Syncro API Usage</h2>
|
|
<p id="api-usage-desc">Rolling 60-second request window</p>
|
|
</div>
|
|
<div style="display:flex;gap:8px;align-items:center;">
|
|
<button class="btn btn-ghost btn-sm" id="btn-history-toggle">
|
|
<span class="history-icon-wrap">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<polyline points="12 6 12 12 16 14"/>
|
|
</svg>
|
|
<span class="btn-history-badge" id="history-badge" hidden>0</span>
|
|
</span>
|
|
History
|
|
</button>
|
|
<button class="btn btn-ghost btn-sm" id="btn-refresh-usage" title="Refresh">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
<polyline points="23 4 23 10 17 10"/>
|
|
<path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/>
|
|
</svg>
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- History panel (collapsible) -->
|
|
<div id="usage-history-panel" class="usage-history-panel" hidden>
|
|
<div class="usage-tf-row">
|
|
<span class="usage-tf-label">Timeframe:</span>
|
|
<button class="usage-tf-chip active" data-window="3600000">1h</button>
|
|
<button class="usage-tf-chip" data-window="86400000">24h</button>
|
|
<button class="usage-tf-chip" data-window="604800000">7d</button>
|
|
</div>
|
|
<div class="usage-chart-wrap">
|
|
<canvas id="usage-chart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="api-usage-body">
|
|
<div class="api-usage-row">
|
|
<div class="api-usage-bar-wrap">
|
|
<div class="api-usage-bar" id="api-usage-bar"></div>
|
|
</div>
|
|
<div class="api-usage-label" id="api-usage-label">— / 180</div>
|
|
</div>
|
|
<div class="api-usage-sub" id="api-usage-sub">Updated just now · auto-refreshes every 10s</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Server section — only visible to superduperadmin -->
|
|
<section class="admin-section" id="section-server" style="display:none;">
|
|
<div class="section-header">
|
|
<div>
|
|
<h2>Server</h2>
|
|
<p>Process management</p>
|
|
</div>
|
|
</div>
|
|
<div class="server-body">
|
|
<div class="server-info">
|
|
<p>Triggers a graceful process exit. PM2 will automatically restart the server within a second or two. Active sessions are preserved — users will not need to log in again.</p>
|
|
</div>
|
|
<button class="btn-restart" id="btn-restart">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="23 4 23 10 17 10"/>
|
|
<path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/>
|
|
</svg>
|
|
Restart Server
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
</main>
|
|
|
|
<footer style="text-align:center;padding:1.25rem;font-size:0.72rem;color:var(--text-muted,#888);">
|
|
© <span id="footer-year"></span> Carmichael Computing
|
|
</footer>
|
|
|
|
<!-- Toast container -->
|
|
<div id="toast-container" aria-live="polite"></div>
|
|
|
|
<!-- ── User modal (create / edit) ── -->
|
|
<div id="user-modal" class="modal-backdrop" style="display:none;" role="dialog" aria-modal="true" aria-labelledby="modal-title">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<h3 id="modal-title">New User</h3>
|
|
<button class="modal-close" id="modal-close" aria-label="Close">×</button>
|
|
</div>
|
|
<form id="user-form" autocomplete="off" novalidate>
|
|
<div class="modal-body">
|
|
<div id="modal-error" class="modal-error" style="display:none;"></div>
|
|
|
|
<div class="form-group">
|
|
<label for="f-name">Full Name</label>
|
|
<input type="text" id="f-name" name="name" autocomplete="off" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="f-username">Username</label>
|
|
<input type="text" id="f-username" name="username" autocomplete="off"
|
|
autocorrect="off" autocapitalize="off" spellcheck="false" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Syncro Customer</label>
|
|
<div class="customer-picker">
|
|
<input type="text" id="f-customer-search" class="customer-search-input" placeholder="Search customers…" autocomplete="off" spellcheck="false">
|
|
<select id="f-syncro-customer" class="customer-listbox" size="5">
|
|
<option value="">— None (internal user) —</option>
|
|
</select>
|
|
</div>
|
|
<span class="form-hint" id="f-customer-hint">Loading customers…</span>
|
|
</div>
|
|
|
|
<div class="form-group" id="fg-role">
|
|
<label for="f-role">Role</label>
|
|
<select id="f-role" name="role" required>
|
|
<option value="client">Client</option>
|
|
<option value="tech">Tech</option>
|
|
<option value="admin">Admin</option>
|
|
<option value="superduperadmin">Super Admin</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="f-password" id="f-password-label">Password</label>
|
|
<input type="password" id="f-password" name="password" autocomplete="new-password">
|
|
<span class="form-hint" id="f-password-hint">Minimum 12 characters.</span>
|
|
</div>
|
|
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-ghost btn-sm" id="modal-cancel">Cancel</button>
|
|
<button type="submit" class="btn btn-primary btn-sm" id="modal-submit">Create User</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Confirm modal ── -->
|
|
<div id="confirm-modal" class="modal-backdrop" style="display:none;" role="dialog" aria-modal="true">
|
|
<div class="modal confirm-modal">
|
|
<div class="modal-header">
|
|
<h3 id="confirm-title">Confirm</h3>
|
|
<button class="modal-close" id="confirm-close" aria-label="Close">×</button>
|
|
</div>
|
|
<div class="confirm-body">
|
|
<p id="confirm-message"></p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-ghost btn-sm" id="confirm-cancel">Cancel</button>
|
|
<button type="button" class="btn btn-sm" id="confirm-ok">Confirm</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/admin/admin.js"></script>
|
|
<script>document.getElementById('footer-year').textContent = new Date().getFullYear();</script>
|
|
</body>
|
|
</html>
|