501 lines
15 KiB
HTML
501 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>APtool Admin</title>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; }
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
margin: 0;
|
|
padding: 16px;
|
|
background: #1a1a2e;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
.container {
|
|
max-width: 600px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* Header */
|
|
.header {
|
|
text-align: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 1.5rem;
|
|
margin: 0 0 4px;
|
|
}
|
|
|
|
.badge {
|
|
display: inline-block;
|
|
background: #2563eb;
|
|
color: #fff;
|
|
font-size: 0.7rem;
|
|
font-weight: 700;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.header a {
|
|
display: block;
|
|
margin-top: 8px;
|
|
color: #999;
|
|
font-size: 0.85rem;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.header a:hover {
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
/* Status banner */
|
|
#status-banner {
|
|
display: none;
|
|
padding: 12px;
|
|
border-radius: 8px;
|
|
text-align: center;
|
|
font-weight: 600;
|
|
font-size: 0.9rem;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
#status-banner.success {
|
|
display: block;
|
|
background: #064e3b;
|
|
color: #6ee7b7;
|
|
}
|
|
|
|
#status-banner.error {
|
|
display: block;
|
|
background: #3b1111;
|
|
color: #fca5a5;
|
|
}
|
|
|
|
/* Cards */
|
|
.card {
|
|
background: #16213e;
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
margin-bottom: 12px;
|
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.card h3 {
|
|
margin: 0 0 8px;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.card .info {
|
|
font-size: 0.9rem;
|
|
color: #aaa;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.card .info span {
|
|
font-weight: 600;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
/* Status badges */
|
|
.nc-badge {
|
|
display: inline-block;
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
margin-left: 4px;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.nc-badge.exists {
|
|
background: #064e3b;
|
|
color: #6ee7b7;
|
|
}
|
|
|
|
.nc-badge.missing {
|
|
background: #3b1111;
|
|
color: #fca5a5;
|
|
}
|
|
|
|
.nc-badge.global {
|
|
background: #1e3a5f;
|
|
color: #93c5fd;
|
|
}
|
|
|
|
/* Buttons */
|
|
.btn {
|
|
display: inline-block;
|
|
padding: 8px 14px;
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
margin-right: 6px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.btn-blue {
|
|
background: #2563eb;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-blue:active { background: #1d4ed8; }
|
|
|
|
.btn-outline {
|
|
background: #16213e;
|
|
color: #2563eb;
|
|
border: 2px solid #2563eb;
|
|
}
|
|
|
|
.btn-outline:active { background: #1a2744; }
|
|
|
|
.btn-red {
|
|
background: #16213e;
|
|
color: #f87171;
|
|
border: 2px solid #dc2626;
|
|
}
|
|
|
|
.btn-red:active { background: #2a1111; }
|
|
|
|
.btn-green {
|
|
background: #059669;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-green:active { background: #047857; }
|
|
|
|
.btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.card-actions {
|
|
margin-top: 10px;
|
|
border-top: 1px solid #2a2a4a;
|
|
padding-top: 10px;
|
|
}
|
|
|
|
/* Add form */
|
|
.add-form h3 {
|
|
margin: 0 0 12px;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.form-row {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.form-row input {
|
|
flex: 1;
|
|
padding: 10px;
|
|
font-size: 0.9rem;
|
|
border: 1px solid #2a2a4a;
|
|
border-radius: 6px;
|
|
background: #0f3460;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
.form-row input:focus {
|
|
outline: none;
|
|
border-color: #2563eb;
|
|
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
|
|
}
|
|
|
|
/* Edit modal overlay */
|
|
.modal-overlay {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
z-index: 1000;
|
|
justify-content: center;
|
|
align-items: center;
|
|
padding: 16px;
|
|
}
|
|
|
|
.modal-overlay.active {
|
|
display: flex;
|
|
}
|
|
|
|
.modal {
|
|
background: #16213e;
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
width: 100%;
|
|
max-width: 400px;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
.modal h3 {
|
|
margin: 0 0 16px;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.modal label {
|
|
display: block;
|
|
font-weight: 600;
|
|
font-size: 0.85rem;
|
|
margin-bottom: 4px;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.modal input {
|
|
width: 100%;
|
|
padding: 10px;
|
|
font-size: 0.9rem;
|
|
border: 1px solid #2a2a4a;
|
|
border-radius: 6px;
|
|
background: #0f3460;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
.modal input:focus {
|
|
outline: none;
|
|
border-color: #2563eb;
|
|
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
|
|
}
|
|
|
|
.modal-actions {
|
|
margin-top: 16px;
|
|
display: flex;
|
|
gap: 8px;
|
|
justify-content: flex-end;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
|
|
<div style="text-align:center;color:#999;font-size:0.8rem;margin-bottom:8px;">JCP Wifi Migration 2026</div>
|
|
|
|
<!-- Header -->
|
|
<div class="header">
|
|
<h1>APtool</h1>
|
|
<span class="badge">Admin</span>
|
|
<a href="/admin/logout">Log out</a>
|
|
</div>
|
|
|
|
<!-- Status banner (shown by JS after operations) -->
|
|
<div id="status-banner"></div>
|
|
|
|
<!-- Add Site card -->
|
|
<div class="card add-form">
|
|
<h3>Add Site</h3>
|
|
<div class="form-row">
|
|
<input type="text" id="add-site" placeholder="Site (4 digits)" maxlength="4" inputmode="numeric">
|
|
<input type="text" id="add-pin" placeholder="PIN" inputmode="numeric">
|
|
</div>
|
|
<div class="form-row">
|
|
<input type="text" id="add-nc-user" placeholder="NC User (optional)">
|
|
<input type="text" id="add-nc-pass" placeholder="NC Pass (optional)">
|
|
</div>
|
|
<button class="btn btn-blue" onclick="addSite()">Add Site</button>
|
|
</div>
|
|
|
|
<!-- Site cards -->
|
|
{% for site in sites %}
|
|
<div class="card site-card" data-id="{{ site.id }}">
|
|
<h3>Site {{ site.id }}</h3>
|
|
<div class="info">PIN: <span>{{ site.pin }}</span></div>
|
|
<div class="info">
|
|
NC User:
|
|
{% if site.nc_user %}
|
|
<span>{{ site.nc_user }}</span>
|
|
{% if site.nc_exists == true %}
|
|
<span class="nc-badge exists">Exists</span>
|
|
{% elif site.nc_exists == false %}
|
|
<span class="nc-badge missing">Not found</span>
|
|
{% endif %}
|
|
{% else %}
|
|
<span class="nc-badge global">Global</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="card-actions">
|
|
{% if site.nc_user and site.nc_exists == false %}
|
|
<button class="btn btn-green" onclick="provisionUser('{{ site.id }}', this)">Create User</button>
|
|
{% endif %}
|
|
<button class="btn btn-outline" onclick="openEdit('{{ site.id }}', '{{ site.pin }}', '{{ site.nc_user or '' }}', '{{ site.nc_pass or '' }}')">Edit</button>
|
|
<button class="btn btn-red" onclick="deleteSite('{{ site.id }}', this)">Delete</button>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
|
|
{% if not sites %}
|
|
<div class="card" style="text-align:center;color:#666;">
|
|
No sites configured. Add one above.
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div style="text-align:center;margin-top:24px;">
|
|
<a href="/login" style="color:#666;font-size:0.8rem;text-decoration:none;">Tech Login</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Modal -->
|
|
<div class="modal-overlay" id="edit-modal">
|
|
<div class="modal">
|
|
<h3>Edit Site <span id="edit-site-id"></span></h3>
|
|
<label for="edit-pin">PIN</label>
|
|
<input type="text" id="edit-pin" inputmode="numeric">
|
|
<label for="edit-nc-user">NC User</label>
|
|
<input type="text" id="edit-nc-user">
|
|
<label for="edit-nc-pass">NC Pass</label>
|
|
<input type="text" id="edit-nc-pass">
|
|
<div class="modal-actions">
|
|
<button class="btn btn-outline" onclick="closeEdit()">Cancel</button>
|
|
<button class="btn btn-blue" onclick="saveEdit()">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const banner = document.getElementById('status-banner');
|
|
|
|
function showBanner(msg, type) {
|
|
banner.textContent = msg;
|
|
banner.className = type; // 'success' or 'error'
|
|
setTimeout(() => { banner.className = ''; banner.textContent = ''; }, 4000);
|
|
}
|
|
|
|
// ---- Add Site ----
|
|
async function addSite() {
|
|
const site = document.getElementById('add-site').value.trim();
|
|
const pin = document.getElementById('add-pin').value.trim();
|
|
const nc_user = document.getElementById('add-nc-user').value.trim();
|
|
const nc_pass = document.getElementById('add-nc-pass').value.trim();
|
|
|
|
if (!site || !pin) {
|
|
showBanner('Site number and PIN are required', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const resp = await fetch('/admin/api/sites', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({site, pin, nc_user, nc_pass})
|
|
});
|
|
const data = await resp.json();
|
|
if (data.ok) {
|
|
window.location.reload();
|
|
} else {
|
|
showBanner(data.error || 'Failed to add site', 'error');
|
|
}
|
|
} catch (err) {
|
|
showBanner('Network error', 'error');
|
|
}
|
|
}
|
|
|
|
// ---- Delete Site ----
|
|
async function deleteSite(id, btn) {
|
|
if (!confirm('Delete site ' + id + '? Uploaded files will be preserved.')) return;
|
|
btn.disabled = true;
|
|
btn.textContent = 'Deleting...';
|
|
|
|
try {
|
|
const resp = await fetch('/admin/api/sites/' + id, {method: 'DELETE'});
|
|
const data = await resp.json();
|
|
if (data.ok) {
|
|
const card = btn.closest('.site-card');
|
|
if (card) card.remove();
|
|
showBanner('Site ' + id + ' deleted', 'success');
|
|
} else {
|
|
showBanner(data.error || 'Failed to delete', 'error');
|
|
btn.disabled = false;
|
|
btn.textContent = 'Delete';
|
|
}
|
|
} catch (err) {
|
|
showBanner('Network error', 'error');
|
|
btn.disabled = false;
|
|
btn.textContent = 'Delete';
|
|
}
|
|
}
|
|
|
|
// ---- Provision NC User ----
|
|
async function provisionUser(id, btn) {
|
|
btn.disabled = true;
|
|
btn.textContent = 'Creating...';
|
|
|
|
try {
|
|
const resp = await fetch('/admin/api/sites/' + id + '/provision', {method: 'POST'});
|
|
const data = await resp.json();
|
|
if (data.ok) {
|
|
showBanner(data.message || 'User created', 'success');
|
|
window.location.reload();
|
|
} else {
|
|
showBanner(data.error || 'Failed to create user', 'error');
|
|
btn.disabled = false;
|
|
btn.textContent = 'Create User';
|
|
}
|
|
} catch (err) {
|
|
showBanner('Network error', 'error');
|
|
btn.disabled = false;
|
|
btn.textContent = 'Create User';
|
|
}
|
|
}
|
|
|
|
// ---- Edit Modal ----
|
|
let editingSiteId = null;
|
|
|
|
function openEdit(id, pin, nc_user, nc_pass) {
|
|
editingSiteId = id;
|
|
document.getElementById('edit-site-id').textContent = id;
|
|
document.getElementById('edit-pin').value = pin;
|
|
document.getElementById('edit-nc-user').value = nc_user;
|
|
document.getElementById('edit-nc-pass').value = nc_pass;
|
|
document.getElementById('edit-modal').classList.add('active');
|
|
}
|
|
|
|
function closeEdit() {
|
|
document.getElementById('edit-modal').classList.remove('active');
|
|
editingSiteId = null;
|
|
}
|
|
|
|
async function saveEdit() {
|
|
if (!editingSiteId) return;
|
|
|
|
const pin = document.getElementById('edit-pin').value.trim();
|
|
const nc_user = document.getElementById('edit-nc-user').value.trim();
|
|
const nc_pass = document.getElementById('edit-nc-pass').value.trim();
|
|
|
|
if (!pin) {
|
|
showBanner('PIN is required', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const resp = await fetch('/admin/api/sites/' + editingSiteId, {
|
|
method: 'PUT',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({pin, nc_user, nc_pass})
|
|
});
|
|
const data = await resp.json();
|
|
if (data.ok) {
|
|
window.location.reload();
|
|
} else {
|
|
showBanner(data.error || 'Failed to save', 'error');
|
|
}
|
|
} catch (err) {
|
|
showBanner('Network error', 'error');
|
|
}
|
|
}
|
|
|
|
// Close modal on overlay click
|
|
document.getElementById('edit-modal').addEventListener('click', function(e) {
|
|
if (e.target === this) closeEdit();
|
|
});
|
|
</script>
|
|
<div style="text-align:center;margin-top:32px;color:#555;font-size:0.7rem;">© 2026 Mack Allison, sdAnywhere LLC (with Claude Code)</div>
|
|
</body>
|
|
</html>
|