feat: hash routing, provision modal, decommission/pause rename, clickable rows
Some checks failed
Build & Release / build (push) Has been cancelled

This commit is contained in:
Ryan Moon
2026-04-03 22:08:39 -05:00
parent 1997a902a7
commit 48d3fa8608

View File

@@ -203,10 +203,6 @@
<span class="nav-icon"></span>
<span>Customers</span>
</div>
<div class="nav-item" id="nav-create-env" onclick="showPage('create-env')">
<span class="nav-icon"></span>
<span>Create Env</span>
</div>
</nav>
<div class="sidebar-user">
<button class="user-btn" onclick="toggleUserMenu()">
@@ -224,7 +220,10 @@
<!-- Customers list -->
<div id="page-customers" class="page">
<div class="page-title">Customers</div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem">
<div class="page-title" style="margin-bottom:0">Customers</div>
<button class="btn btn-primary" onclick="openProvisionModal()">+ New Environment</button>
</div>
<div class="table-toolbar">
<input class="search-input" id="customers-search" type="text" placeholder="Search customers…" oninput="onSearchInput()" />
<button class="btn btn-slate" onclick="loadCustomers()">Refresh</button>
@@ -263,7 +262,7 @@
<!-- Customer Detail -->
<div id="page-customer-detail" class="page">
<div style="margin-bottom:1.2rem">
<button class="btn btn-slate btn-sm" onclick="showPage('customers')">← Customers</button>
<button class="btn btn-slate btn-sm" onclick="navigate('customers')">← Customers</button>
</div>
<div class="detail-header">
<div style="flex:1">
@@ -273,7 +272,7 @@
</div>
<div style="display:flex;gap:0.5rem;align-items:center">
<button class="btn btn-slate btn-sm" onclick="refreshDetail()">↻ Refresh Status</button>
<button class="btn btn-danger btn-sm" id="detail-delete-btn" onclick="">Delete</button>
<button class="btn btn-danger btn-sm" id="detail-delete-btn" onclick="">Decommission</button>
</div>
</div>
<div class="detail-grid" id="detail-grid">
@@ -284,10 +283,54 @@
</div>
</div>
<!-- Create Env -->
<div id="page-create-env" class="page">
<div class="page-title">Create Environment</div>
<!-- Account -->
<div id="page-account" class="page">
<div class="page-title">Account</div>
<div class="card">
<div class="form-group">
<label for="new-password">New Password</label>
<input id="new-password" type="password" autocomplete="new-password" />
</div>
<div class="form-group">
<label for="confirm-password">Confirm Password</label>
<input id="confirm-password" type="password" autocomplete="new-password" />
</div>
<button class="btn btn-slate" onclick="changePassword()">Update Password</button>
<div id="account-status" class="status"></div>
</div>
</div>
</main>
</div>
</div>
<!-- Kebab dropdown (shared, repositioned via JS) -->
<div id="kebab-menu">
<div class="kebab-item" onclick="kebabAction('view')">View Details</div>
<div class="kebab-item" id="kebab-deactivate" onclick="kebabAction('deactivate')">Pause</div>
<div class="kebab-item" id="kebab-reactivate" onclick="kebabAction('reactivate')" style="display:none">Resume</div>
<div class="kebab-item" onclick="kebabAction('upgrade')">Upgrade Chart</div>
<div class="kebab-item danger" onclick="kebabAction('delete')">Decommission</div>
<div class="kebab-item" onclick="kebabAction('record')">Remove Record Only</div>
</div>
<!-- Decommission confirm dialog -->
<div class="overlay" id="delete-overlay" onclick="if(event.target===this)closeDeleteDialog()">
<div class="dialog">
<h3>Decommission Customer</h3>
<p>This will permanently remove the deployment, database, storage, and all associated resources for <strong id="delete-slug-label" style="color:#e6edf3"></strong>. This cannot be undone.</p>
<div class="dialog-actions">
<button class="btn btn-slate" onclick="closeDeleteDialog()">Cancel</button>
<button class="btn btn-danger" id="delete-confirm-btn" onclick="confirmDelete()">Decommission</button>
</div>
</div>
</div>
<!-- Provision modal -->
<div class="overlay" id="provision-overlay" onclick="if(event.target===this)closeProvisionModal()">
<div class="dialog" style="max-width:480px;width:100%">
<h3>New Environment</h3>
<div class="form-group">
<label for="customer-name">Customer Name</label>
<input id="customer-name" type="text" placeholder="Acme Shop" />
@@ -317,50 +360,10 @@
<label class="module-check" id="mod-accounting"><input type="checkbox" value="accounting" onchange="toggleModule(this)" /> Accounting</label>
</div>
</div>
<button class="btn btn-primary" id="provision-btn" onclick="provision()" style="margin-top:0.4rem">Provision</button>
<div id="provision-status" class="status"></div>
</div>
</div>
<!-- Account -->
<div id="page-account" class="page">
<div class="page-title">Account</div>
<div class="card">
<div class="form-group">
<label for="new-password">New Password</label>
<input id="new-password" type="password" autocomplete="new-password" />
</div>
<div class="form-group">
<label for="confirm-password">Confirm Password</label>
<input id="confirm-password" type="password" autocomplete="new-password" />
</div>
<button class="btn btn-slate" onclick="changePassword()">Update Password</button>
<div id="account-status" class="status"></div>
</div>
</div>
</main>
</div>
</div>
<!-- Kebab dropdown (shared, repositioned via JS) -->
<div id="kebab-menu">
<div class="kebab-item" onclick="kebabAction('view')">View Details</div>
<div class="kebab-item" id="kebab-deactivate" onclick="kebabAction('deactivate')">Deactivate</div>
<div class="kebab-item" id="kebab-reactivate" onclick="kebabAction('reactivate')" style="display:none">Reactivate</div>
<div class="kebab-item" onclick="kebabAction('upgrade')">Upgrade Chart</div>
<div class="kebab-item danger" onclick="kebabAction('delete')">Delete</div>
<div class="kebab-item" onclick="kebabAction('record')">Remove Record Only</div>
</div>
<!-- Delete confirm dialog -->
<div class="overlay" id="delete-overlay" onclick="if(event.target===this)closeDeleteDialog()">
<div class="dialog">
<h3>Delete Customer</h3>
<p>This will remove the ArgoCD app, database, storage, and all associated resources for <strong id="delete-slug-label" style="color:#e6edf3"></strong>. This cannot be undone.</p>
<div class="dialog-actions">
<button class="btn btn-slate" onclick="closeDeleteDialog()">Cancel</button>
<button class="btn btn-danger" id="delete-confirm-btn" onclick="confirmDelete()">Delete</button>
<button class="btn btn-slate" onclick="closeProvisionModal()">Cancel</button>
<button class="btn btn-primary" id="provision-btn" onclick="provision()">Provision</button>
</div>
</div>
</div>
@@ -389,7 +392,7 @@
if (res.ok) {
const data = await res.json();
setUser(data.username);
showPage('customers');
handleRoute();
loadCustomers();
} else {
localStorage.removeItem('token');
@@ -424,7 +427,7 @@
localStorage.setItem('token', data.token);
setUser(data.username);
document.getElementById('login-view').style.display = 'none';
showPage('customers');
handleRoute();
loadCustomers();
} catch (err) {
status.textContent = err.message;
@@ -445,6 +448,10 @@
// ── Navigation ────────────────────────────────────────────────────────────
function navigate(path) {
window.location.hash = path;
}
function showPage(name) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
@@ -455,6 +462,23 @@
closeKebab();
}
function handleRoute() {
const hash = window.location.hash.slice(1) || 'customers';
if (hash.startsWith('customers/')) {
const slug = hash.slice('customers/'.length);
showPage('customers');
openDetail(slug);
} else if (hash === 'customers') {
showPage('customers');
} else if (hash === 'account') {
showPage('account');
} else {
showPage('customers');
}
}
window.addEventListener('hashchange', handleRoute);
// ── Customers table ───────────────────────────────────────────────────────
async function loadCustomers() {
@@ -496,7 +520,7 @@
const expiry = r.expiration_date
? `<span class="${expired ? 'tag tag-expired' : ''}">${fmtDate(r.expiration_date)}</span>`
: '<span style="color:#484f58">—</span>';
return `<tr style="cursor:pointer" ondblclick="openDetail('${r.slug}')">
return `<tr style="cursor:pointer" onclick="navigate('customers/${r.slug}')">
<td style="font-weight:500;color:#e6edf3">${r.slug}</td>
<td>${r.name || '<span style="color:#484f58">—</span>'}</td>
<td>${customerStatusBadge(r)}</td>
@@ -610,6 +634,16 @@
loadDetail(slug, false);
}
function openProvisionModal() {
document.getElementById('provision-status').className = 'status';
document.getElementById('provision-status').textContent = '';
document.getElementById('provision-overlay').classList.add('open');
}
function closeProvisionModal() {
document.getElementById('provision-overlay').classList.remove('open');
}
function refreshDetail() {
if (currentDetailSlug) loadDetail(currentDetailSlug, true);
}
@@ -803,18 +837,18 @@
throw new Error(d.message ?? 'Delete failed');
}
closeDeleteDialog();
if (currentDetailSlug === slug) showPage('customers');
if (currentDetailSlug === slug) navigate('customers');
loadCustomers();
} catch (err) {
alert(err.message);
} finally {
btn.disabled = false;
btn.textContent = 'Delete';
btn.textContent = 'Decommission';
}
}
async function deactivate(slug) {
if (!confirm(`Deactivate "${slug}"?\n\nThis removes the deployment but keeps the database and storage. You can reactivate later.`)) return;
if (!confirm(`Pause "${slug}"?\n\nThis removes the deployment but keeps the database and storage. You can resume later.`)) return;
const res = await apiFetch(`/api/customers/${slug}/deactivate`, { method: 'POST' });
if (res.ok) { loadCustomers(); if (currentDetailSlug === slug) loadDetail(slug, false); }
else { const d = await res.json().catch(() => ({})); alert(d.message ?? 'Failed'); }
@@ -831,7 +865,7 @@
if (!confirm(`Remove "${slug}" from the manager database only?\n\nThis will NOT delete the ArgoCD app or DO database.`)) return;
const res = await apiFetch(`/api/customers/${slug}/record`, { method: 'DELETE' });
if (res.ok) {
if (currentDetailSlug === slug) showPage('customers');
if (currentDetailSlug === slug) navigate('customers');
loadCustomers();
} else {
const d = await res.json().catch(() => ({}));
@@ -902,12 +936,12 @@
});
const data = await res.json();
if (!res.ok) throw new Error(data.message ?? 'Unknown error');
status.textContent = `${data.slug} provisioned successfully`;
status.className = 'status success';
closeProvisionModal();
document.getElementById('customer-name').value = '';
document.getElementById('slug').value = '';
document.getElementById('expiration-date').value = '';
document.querySelectorAll('.module-check').forEach(l => { l.classList.remove('checked'); l.querySelector('input').checked = false; });
loadCustomers();
} catch (err) {
status.textContent = `${err.message}`;
status.className = 'status error';