fix: persist auth cookie across refreshes and add customer detail tracking
Some checks failed
Build & Release / build (push) Has been cancelled

- Fix cookie sameSite strict → lax so browser sends it on page refresh
- Add customer name field (separate from slug)
- Add steps JSONB column tracking per-step provisioning state (DB, User, Schema, Pool, Chart)
- Insert customer record before provisioning starts so partial failures are visible
- Show status + step checklist in customers table
- Add DELETE /customers/:slug/record endpoint to clear failed records without touching infra
- Add "Record Only" button in UI for manual cleanup of partial deployments
This commit is contained in:
Ryan Moon
2026-04-03 18:00:46 -05:00
parent 7e63dbac9c
commit accc963883
4 changed files with 100 additions and 17 deletions

View File

@@ -339,6 +339,8 @@
<thead>
<tr>
<th class="sortable sort-active" data-col="slug" onclick="setSort('slug')">Slug <span class="sort-icon" id="sort-icon-slug"></span></th>
<th>Name</th>
<th>Status</th>
<th>Modules</th>
<th class="sortable" data-col="start_date" onclick="setSort('start_date')">Start <span class="sort-icon" id="sort-icon-start_date"></span></th>
<th class="sortable" data-col="expiration_date" onclick="setSort('expiration_date')">Expires <span class="sort-icon" id="sort-icon-expiration_date"></span></th>
@@ -348,7 +350,7 @@
</tr>
</thead>
<tbody id="customers-tbody">
<tr class="empty-row"><td colspan="7">Loading…</td></tr>
<tr class="empty-row"><td colspan="9">Loading…</td></tr>
</tbody>
</table>
<div class="pagination" id="customers-pagination" style="display:none">
@@ -367,7 +369,11 @@
<div class="page-title">Create Environment</div>
<div class="card">
<div class="form-group">
<label for="slug">Customer Slug</label>
<label for="customer-name">Customer Name</label>
<input id="customer-name" type="text" placeholder="Acme Shop" />
</div>
<div class="form-group">
<label for="slug">Slug</label>
<input id="slug" type="text" placeholder="acme-shop" pattern="[a-z0-9-]+" />
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.8rem">
@@ -530,7 +536,7 @@
function renderTable(rows, pagination) {
const tbody = document.getElementById('customers-tbody');
if (rows.length === 0) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="7">No customers found</td></tr>';
tbody.innerHTML = '<tr class="empty-row"><td colspan="9">No customers found</td></tr>';
} else {
tbody.innerHTML = rows.map(r => {
const expired = r.expiration_date && new Date(r.expiration_date) < new Date();
@@ -538,14 +544,30 @@
const expiry = r.expiration_date
? `<span class="${expired ? 'tag tag-expired' : ''}">${fmtDate(r.expiration_date)}</span>`
: '<span style="color:#484f58">—</span>';
const statusColor = r.status === 'provisioned' ? '#3fb950' : r.status === 'failed' ? '#f85149' : '#e3a04a';
const STEP_LABELS = { database: 'DB', database_user: 'User', database_setup: 'Schema', pool: 'Pool', chart: 'Chart' };
const stepsHtml = Object.entries(STEP_LABELS).map(([key, label]) => {
const s = (r.steps || {})[key];
const icon = s === 'done' ? '✓' : s === 'failed' ? '✗' : '·';
const color = s === 'done' ? '#3fb950' : s === 'failed' ? '#f85149' : '#484f58';
return `<span style="color:${color};font-size:0.75rem;margin-right:6px" title="${key}">${icon} ${label}</span>`;
}).join('');
return `<tr>
<td style="font-weight:500;color:#e6edf3">${r.slug}</td>
<td>${r.name || '<span style="color:#484f58">—</span>'}</td>
<td>
<div style="color:${statusColor};font-size:0.78rem;font-weight:500;margin-bottom:3px">${r.status}</div>
<div>${stepsHtml}</div>
</td>
<td>${modules}</td>
<td>${fmtDate(r.start_date)}</td>
<td>${expiry}</td>
<td style="color:#8b949e">${fmtDateTime(r.created_at)}</td>
<td style="color:#8b949e">${fmtDateTime(r.updated_at)}</td>
<td><button class="btn btn-danger btn-sm" onclick="openDeleteDialog('${r.slug}')">Delete</button></td>
<td style="display:flex;gap:0.4rem;align-items:center">
<button class="btn btn-danger btn-sm" onclick="openDeleteDialog('${r.slug}')">Delete</button>
<button class="btn btn-slate btn-sm" onclick="removeRecord('${r.slug}')" title="Remove from manager DB only">Record Only</button>
</td>
</tr>`;
}).join('');
}
@@ -635,6 +657,17 @@
}
}
async function removeRecord(slug) {
if (!confirm(`Remove "${slug}" from the manager database only?\n\nThis will NOT delete the ArgoCD app or DO database — use this only for failed partial deployments.`)) return;
const res = await fetch(`/api/customers/${slug}/record`, { method: 'DELETE' });
if (res.ok) {
loadCustomers();
} else {
const d = await res.json().catch(() => ({}));
alert(d.message ?? 'Failed to remove record');
}
}
// ── Provision ─────────────────────────────────────────────────────────────
function toggleModule(checkbox) {
@@ -643,10 +676,12 @@
}
async function provision() {
const name = document.getElementById('customer-name').value.trim();
const slug = document.getElementById('slug').value.trim();
const startDate = document.getElementById('start-date').value;
const expirationDate = document.getElementById('expiration-date').value || null;
const modules = [...document.querySelectorAll('.module-check input:checked')].map(el => el.value);
if (!name) return;
const btn = document.getElementById('provision-btn');
const status = document.getElementById('provision-status');
if (!slug) return;
@@ -657,12 +692,13 @@
const res = await fetch('/api/customers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: slug, modules, startDate, expirationDate }),
body: JSON.stringify({ slug, name, modules, startDate, expirationDate }),
});
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';
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; });