asset_browser/public/admin/index.html
2026-03-27 09:18:39 -04:00

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);">
&copy; <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">&times;</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">&times;</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>