From 48d3fa86087831ff44e96a198349d0c66dbc7a88 Mon Sep 17 00:00:00 2001 From: Ryan Moon Date: Fri, 3 Apr 2026 22:08:39 -0500 Subject: [PATCH] feat: hash routing, provision modal, decommission/pause rename, clickable rows --- frontend/index.html | 154 +++++++++++++++++++++++++++----------------- 1 file changed, 94 insertions(+), 60 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 0405fad..37e153b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -203,10 +203,6 @@ Customers -
@@ -263,7 +262,7 @@
- +
@@ -273,7 +272,7 @@
- +
@@ -284,43 +283,6 @@
- -
-
Create Environment
-
-
- - -
-
- - -
-
-
- - -
-
- - -
-
-
- -
- - - - - - -
-
- -
-
-
@@ -346,21 +308,62 @@
View Details
-
Deactivate
- +
Pause
+
Upgrade Chart
-
Delete
+
Decommission
Remove Record Only
- +
-

Delete Customer

-

This will remove the ArgoCD app, database, storage, and all associated resources for . This cannot be undone.

+

Decommission Customer

+

This will permanently remove the deployment, database, storage, and all associated resources for . This cannot be undone.

- + +
+
+
+ + +
+
+

New Environment

+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ +
+ + + + + + +
+
+
+
+ +
@@ -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 ? `${fmtDate(r.expiration_date)}` : ''; - return ` + return ` ${r.slug} ${r.name || ''} ${customerStatusBadge(r)} @@ -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';