merge: Logistics/Tickets tabbed navigation into staging
This commit is contained in:
commit
d907ea9425
2 changed files with 267 additions and 1 deletions
173
docs/webhook-auto-deploy.html
Normal file
173
docs/webhook-auto-deploy.html
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Automatic Deploys — Forgejo → Coolify (FleetOps / FleetNow)</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg:#161a23; --panel:#1e232e; --panel-2:#232a36; --border:#2c333f;
|
||||
--text:#ECEFF4; --muted:#93a0b4; --accent:#E8954A; --live:#2dd4a7;
|
||||
--warn:#f0a93b; --danger:#ef5b5b;
|
||||
}
|
||||
* { box-sizing:border-box; }
|
||||
body {
|
||||
margin:0; background:var(--bg); color:var(--text);
|
||||
font:15px/1.6 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
|
||||
}
|
||||
.wrap { max-width:860px; margin:0 auto; padding:40px 22px 80px; }
|
||||
header.doc { display:flex; align-items:center; gap:10px; margin-bottom:6px; }
|
||||
header.doc .mark { width:11px; height:11px; border-radius:50%; background:var(--accent); box-shadow:0 0 10px var(--accent); }
|
||||
header.doc .brand { font-weight:800; letter-spacing:.5px; font-size:17px; }
|
||||
header.doc .brand .nm { color:var(--accent); }
|
||||
.sub { color:var(--muted); font-size:13px; margin:0 0 28px; }
|
||||
h1 { font-size:24px; margin:18px 0 6px; }
|
||||
h2 {
|
||||
font-size:13px; text-transform:uppercase; letter-spacing:.8px; color:var(--accent);
|
||||
margin:34px 0 10px; padding-bottom:6px; border-bottom:1px solid var(--border);
|
||||
}
|
||||
h3 { font-size:15px; margin:22px 0 6px; }
|
||||
p { margin:8px 0; }
|
||||
code, kbd {
|
||||
background:var(--panel-2); border:1px solid var(--border); border-radius:4px;
|
||||
padding:1px 6px; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:13px;
|
||||
}
|
||||
kbd { color:var(--accent); }
|
||||
pre {
|
||||
background:var(--panel); border:1px solid var(--border); border-radius:8px;
|
||||
padding:14px 16px; overflow:auto; font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
|
||||
font-size:13px; line-height:1.5;
|
||||
}
|
||||
ol, ul { margin:8px 0; padding-left:22px; }
|
||||
li { margin:5px 0; }
|
||||
table { width:100%; border-collapse:collapse; margin:12px 0; font-size:14px; }
|
||||
th, td { text-align:left; padding:9px 11px; border-bottom:1px solid var(--border); vertical-align:top; }
|
||||
th { color:var(--muted); font-size:11px; text-transform:uppercase; letter-spacing:.5px; background:var(--panel-2); }
|
||||
.note { background:var(--panel); border:1px solid var(--border); border-left:3px solid var(--accent); border-radius:6px; padding:12px 14px; margin:14px 0; }
|
||||
.warn { border-left-color:var(--warn); }
|
||||
.danger { border-left-color:var(--danger); }
|
||||
.ok { color:var(--live); font-weight:600; }
|
||||
.bad { color:var(--danger); font-weight:600; }
|
||||
.step { background:var(--panel); border:1px solid var(--border); border-radius:8px; padding:4px 18px 8px; margin:12px 0; }
|
||||
a { color:var(--accent); }
|
||||
footer { margin-top:48px; color:var(--muted); font-size:12px; border-top:1px solid var(--border); padding-top:14px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="doc">
|
||||
<span class="mark"></span>
|
||||
<span class="brand">FLEET<span class="nm">OPS</span></span>
|
||||
</header>
|
||||
<p class="sub">Operations runbook · Automatic deploys via Forgejo → Coolify webhooks</p>
|
||||
|
||||
<h1>Automatic deploys: push → live</h1>
|
||||
<p>
|
||||
Every Coolify app (FleetOps + FleetNow, staging & prod) deploys automatically on a
|
||||
<code>git push</code>. The chain is:
|
||||
</p>
|
||||
<pre>git push → Forgejo webhook → Coolify rebuilds the app whose branch matches → Traefik serves the new container</pre>
|
||||
<p>
|
||||
Each Coolify <em>application</em> tracks one branch. A push to that branch fires the webhook;
|
||||
Coolify rebuilds only the app(s) bound to the pushed branch. So <code>staging</code> pushes
|
||||
deploy the <code>*.fivetitude.com</code> apps, and <code>main</code> merges deploy the
|
||||
<code>*.rahamafresh.com</code> (prod) apps.
|
||||
</p>
|
||||
|
||||
<div class="note warn">
|
||||
<strong>Prerequisites on the Coolify app</strong> before the webhook will do anything:
|
||||
<ul>
|
||||
<li><strong>Auto Deploy = ON</strong> — under <kbd>Configuration → Advanced → Auto Deploy</kbd>
|
||||
(<em>not</em> General). The webhook can return 2xx but Coolify ignores it if this is off.</li>
|
||||
<li><strong>Branch</strong> set correctly (<code>staging</code> vs <code>main</code>).</li>
|
||||
<li><strong>Ports Exposes = 80</strong> (both FleetOps/Caddy and FleetNow/nginx listen on 80).</li>
|
||||
<li><strong>Domain</strong> spelled correctly and attached.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>Part A — Get the webhook URL + secret from Coolify</h2>
|
||||
<div class="step">
|
||||
<ol>
|
||||
<li>Open the application in Coolify.</li>
|
||||
<li>Confirm <kbd>Configuration → Advanced → Auto Deploy</kbd> is <span class="ok">ON</span>.</li>
|
||||
<li>Go to the app's <strong>Webhooks</strong> tab → <strong>Manual Git Webhooks</strong> →
|
||||
the <strong>Gitea</strong> section (Forgejo is Gitea-compatible).</li>
|
||||
<li>Copy the <strong>Webhook URL</strong> (e.g.
|
||||
<code>https://<coolify-domain>/webhooks/source/gitea/events/manual</code>)
|
||||
and the <strong>Webhook Secret</strong>.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<h2>Part B — Add the webhook in Forgejo</h2>
|
||||
<div class="step">
|
||||
<ol>
|
||||
<li>Open the repo → <kbd>Settings → Webhooks → Add Webhook → Forgejo/Gitea</kbd>.</li>
|
||||
<li>Fill in:
|
||||
<table>
|
||||
<tr><th>Field</th><th>Value</th></tr>
|
||||
<tr><td>Target URL</td><td>the Coolify Webhook URL (Part A)</td></tr>
|
||||
<tr><td>HTTP Method</td><td><code>POST</code></td></tr>
|
||||
<tr><td>POST Content Type</td><td><code>application/json</code></td></tr>
|
||||
<tr><td>Secret</td><td>the Coolify Webhook Secret (must match exactly)</td></tr>
|
||||
<tr><td>Trigger On</td><td>Push Events (or Custom → Push only)</td></tr>
|
||||
<tr><td>Branch filter</td><td>the app's branch (<code>staging</code> or <code>main</code>) — optional but tidy</td></tr>
|
||||
<tr><td>Active</td><td>checked</td></tr>
|
||||
</table>
|
||||
</li>
|
||||
<li>Click <strong>Add Webhook</strong>.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<h2>Part C — Verify the delivery</h2>
|
||||
<div class="step">
|
||||
<ol>
|
||||
<li>In Forgejo, go to <kbd>repo → Settings → Webhooks</kbd>. Each webhook shows a status dot:
|
||||
<span class="ok">green</span> = last delivery OK, <span class="bad">red</span> = failed.</li>
|
||||
<li><strong>Click the webhook</strong> to open its page, then click <strong>Test Delivery</strong> (top-right).</li>
|
||||
<li>Scroll to <strong>Recent Deliveries</strong>. Click a delivery row to expand it →
|
||||
open the <strong>Response</strong> tab → the HTTP status is there.
|
||||
<span class="ok">2xx = good.</span></li>
|
||||
<li>Confirm a deployment started in the Coolify app's <strong>Deployments</strong> list.</li>
|
||||
<li>Real test: push a trivial change to the branch and confirm <em>only</em> the matching app redeploys.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<h2>Multiple environments (staging + prod)</h2>
|
||||
<p>
|
||||
There is one Coolify app per environment, each on its own branch, and
|
||||
<strong>each app has its own webhook secret</strong>. Add a <em>separate</em> Forgejo webhook per app
|
||||
(same repo), each with its own URL/secret and branch filter:
|
||||
</p>
|
||||
<table>
|
||||
<tr><th>App</th><th>Domain</th><th>Branch</th></tr>
|
||||
<tr><td>FleetOps staging</td><td>fleetops.fivetitude.com</td><td><code>staging</code></td></tr>
|
||||
<tr><td>FleetOps prod</td><td>fleetops.rahamafresh.com</td><td><code>main</code></td></tr>
|
||||
<tr><td>FleetNow staging</td><td>fleetnow.fivetitude.com</td><td><code>staging</code></td></tr>
|
||||
<tr><td>FleetNow prod</td><td>fleetnow.rahamafresh.com</td><td><code>main</code></td></tr>
|
||||
</table>
|
||||
<p>Promotion: <code>feature → staging</code> (auto-deploys staging) → <code>main</code> (auto-deploys prod).</p>
|
||||
|
||||
<h2>Troubleshooting</h2>
|
||||
<table>
|
||||
<tr><th>Symptom</th><th>Cause & fix</th></tr>
|
||||
<tr><td>Delivery 2xx but no deploy</td><td><strong>Auto Deploy off.</strong> Turn it on at <kbd>Configuration → Advanced → Auto Deploy</kbd>.</td></tr>
|
||||
<tr><td>Delivery <span class="bad">401 / 403</span></td><td>Secret mismatch — re-copy the Coolify secret into the Forgejo webhook's Secret field.</td></tr>
|
||||
<tr><td>Delivery <span class="bad">404 / 502</span> on the webhook</td><td>Wrong Target URL, or Coolify unreachable.</td></tr>
|
||||
<tr><td>Site returns <span class="bad">502</span></td><td>Coolify "Ports Exposes" ≠ container port. Set it to <strong>80</strong> and redeploy.</td></tr>
|
||||
<tr><td>Site returns <span class="bad">503</span> / self-signed cert</td><td>No healthy backend yet, or the domain doesn't match the Traefik rule (often a <strong>domain typo</strong>). Fix the domain and redeploy.</td></tr>
|
||||
<tr><td>Deploys, but it's the wrong code</td><td><strong>Wrong branch.</strong> Set the app's Branch (e.g. <code>staging</code>) in the Source config, Save, then redeploy (Force rebuild).</td></tr>
|
||||
</table>
|
||||
|
||||
<div class="note danger">
|
||||
<strong>Never point a prod app at a non-prod branch.</strong> The prod apps
|
||||
(<code>*.rahamafresh.com</code>) must stay on <code>main</code>. Pointing one at <code>staging</code>
|
||||
would push unreviewed code to the client's live site.
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
FleetOps · Fireside Communications fleet stack. See also
|
||||
<code>docs/STAGING_FLEETOPS_ARCHITECTURE.md</code> in the tracksolid repo for the full topology.
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
}
|
||||
.app {
|
||||
display: grid; min-height: 100vh;
|
||||
grid-template-rows: auto auto 1fr; /* header · filter bar · content */
|
||||
grid-template-rows: auto 1fr; /* header · content (tabs/filters live inside each view) */
|
||||
}
|
||||
|
||||
/* ── Top bar (mirrors FleetNow) ──────────────────────────────────────── */
|
||||
|
|
@ -84,6 +84,23 @@
|
|||
.clock .label { font-size: 9.5px; color: var(--muted); text-transform: uppercase; letter-spacing: .6px; }
|
||||
.clock b { font-weight: 600; }
|
||||
|
||||
/* ── Tab nav (segmented control) ─────────────────────────────────────── */
|
||||
.tabs {
|
||||
display: flex; gap: 4px; background: var(--bg);
|
||||
border: 1px solid var(--border); border-radius: 8px; padding: 3px;
|
||||
}
|
||||
.tab {
|
||||
background: transparent; color: var(--muted); border: 0; border-radius: 6px;
|
||||
padding: 6px 14px; font-size: 12.5px; font-weight: 700; letter-spacing: .3px;
|
||||
cursor: pointer; white-space: nowrap;
|
||||
}
|
||||
.tab:hover { color: var(--text); }
|
||||
.tab.active { background: var(--accent); color: #1a1009; }
|
||||
|
||||
/* ── Tabbed views ────────────────────────────────────────────────────── */
|
||||
.view { display: none; }
|
||||
.view.active { display: block; }
|
||||
|
||||
/* ── Filter bar ──────────────────────────────────────────────────────── */
|
||||
.filterbar {
|
||||
padding: 10px 18px; background: var(--panel-2);
|
||||
|
|
@ -164,11 +181,17 @@
|
|||
<div class="app">
|
||||
<header>
|
||||
<div class="brand"><span class="mark"></span>FLEET<span class="nm">OPS</span></div>
|
||||
<nav class="tabs" id="tabs">
|
||||
<button class="tab active" data-tab="logistics" type="button">Logistics</button>
|
||||
<button class="tab" data-tab="tickets" type="button">Tickets</button>
|
||||
</nav>
|
||||
<div id="kpis"></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="clock"><span class="label">EAT</span><b id="clock-time">—</b></div>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<section class="view active" id="view-logistics">
|
||||
<div class="filterbar">
|
||||
<div class="ff">
|
||||
<label for="f-cc">Cost centre</label>
|
||||
|
|
@ -214,6 +237,31 @@
|
|||
<div class="tbl-scroll" id="drv-wrap"><div class="empty">Loading…</div></div>
|
||||
</div>
|
||||
</main>
|
||||
</section>
|
||||
|
||||
<section class="view" id="view-tickets">
|
||||
<main id="main-tickets">
|
||||
<div class="card span12">
|
||||
<div class="banner" id="tk-banner">
|
||||
Tickets data source not connected yet — this tab is scaffolded and ready to wire.
|
||||
<ul>
|
||||
<li>Ticket data must be served via <code>dashboard_api</code> (proxied / presigned from the rustfs <code>tickets</code> bucket); credentials are never embedded in this static SPA.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card span8">
|
||||
<h2>Recent tickets <span class="count" id="tk-count"></span></h2>
|
||||
<div class="tbl-scroll" id="tk-wrap"><div class="empty">Loading…</div></div>
|
||||
</div>
|
||||
|
||||
<div class="card span4">
|
||||
<h2>By status</h2>
|
||||
<div id="tk-status"><div class="empty">Loading…</div></div>
|
||||
</div>
|
||||
</main>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
|
@ -433,6 +481,7 @@ async function loadAll() {
|
|||
api(`/analytics/fuel?${q}`),
|
||||
]);
|
||||
const fuelL = ((fuel && fuel.rows) || []).reduce((s, r) => s + Number(r.actual_fuel_l || 0), 0);
|
||||
lastTotals = summary.totals; lastFuelL = fuelL;
|
||||
renderKpis(summary.totals, fuelL);
|
||||
renderTrend(util.daily_trend);
|
||||
renderVehicles(summary.rows);
|
||||
|
|
@ -447,6 +496,50 @@ async function loadAll() {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TABS (Logistics ↔ Tickets)
|
||||
// ============================================================================
|
||||
// The header KPI strip is shared, so we cache the last logistics totals and
|
||||
// re-render them when switching back from Tickets.
|
||||
let lastTotals = null, lastFuelL = 0;
|
||||
const ticketStats = {}; // populated once a tickets data source is wired
|
||||
|
||||
function renderTicketKpis() {
|
||||
const k = [
|
||||
['accent', ticketStats.open ?? '—', 'Open'],
|
||||
['warn', ticketStats.in_progress ?? '—', 'In progress'],
|
||||
['live', ticketStats.resolved ?? '—', 'Resolved'],
|
||||
['', ticketStats.avg_resolution_h != null ? num(ticketStats.avg_resolution_h, 1) + 'h' : '—', 'Avg resolution'],
|
||||
];
|
||||
$('kpis').innerHTML = k.map(([c, v, l]) =>
|
||||
`<div class="kpi"><b class="${c}">${v}</b><span>${l}</span></div>`).join('');
|
||||
}
|
||||
|
||||
let ticketsLoaded = false;
|
||||
async function loadTickets() {
|
||||
// Integration point. FleetOps is a credential-less static SPA, so ticket data
|
||||
// must arrive through dashboard_api (proxied / presigned from the rustfs
|
||||
// `tickets` bucket) — never by embedding S3 keys here. Wire the fetch + the
|
||||
// renderers below once that endpoint exists. Until then, show empty states.
|
||||
ticketsLoaded = true;
|
||||
$('tk-count').textContent = '';
|
||||
$('tk-wrap').innerHTML = '<div class="empty">No ticket data source connected yet.</div>';
|
||||
$('tk-status').innerHTML = '<div class="empty">—</div>';
|
||||
}
|
||||
|
||||
function switchTab(name) {
|
||||
document.querySelectorAll('.tab').forEach(b => b.classList.toggle('active', b.dataset.tab === name));
|
||||
document.querySelectorAll('.view').forEach(v => v.classList.toggle('active', v.id === `view-${name}`));
|
||||
if (name === 'tickets') {
|
||||
renderTicketKpis();
|
||||
if (!ticketsLoaded) loadTickets();
|
||||
} else {
|
||||
renderKpis(lastTotals, lastFuelL);
|
||||
}
|
||||
}
|
||||
document.querySelectorAll('.tab').forEach(b =>
|
||||
b.addEventListener('click', () => switchTab(b.dataset.tab)));
|
||||
|
||||
// ============================================================================
|
||||
// BOOT
|
||||
// ============================================================================
|
||||
|
|
|
|||
Loading…
Reference in a new issue