fix: persist auth cookie across refreshes and add customer detail tracking
Some checks failed
Build & Release / build (push) Has been cancelled
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:
@@ -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; });
|
||||
|
||||
Reference in New Issue
Block a user