Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The format is intentionally lightweight and human-readable. Group entries by rel
- Added stronger update-alert metadata to `GET /api/update`, including update type, alert level, and recommended action for operators and dashboard consumers
- Added an opt-in `auto_update` policy block plus `foundrygate-auto-update` so controlled deployments can gate helper-driven updates without enabling silent self-updates
- Added `GET /api/operator-events` plus operator-event metrics for update checks and helper-driven auto-update attempts
- Added dashboard cards and tables for operator-side update checks and apply attempts

## v0.6.0 - 2026-03-12

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,8 @@ For image-capable providers, `image.policy_tags` can be used as lightweight pres

`GET /api/stats`, `GET /api/recent`, and `GET /api/traces` also accept optional `provider`, `modality`, `client_profile`, `client_tag`, `layer`, and `success` filters. The built-in dashboard uses the same filtered endpoints.

`GET /api/operator-events` returns recent operator-side update checks and apply attempts. The built-in dashboard now shows both a recent operator-action summary card and an operator-action breakdown table.

`GET /api/traces` returns recent enriched routing records from the metrics store, including requested model, modality, resolved client profile, client tag, decision reason, confidence, and attempt order.

`GET /api/update` returns the cached release-check result for the running service, including the current version, latest known tag, update availability, update type (`patch`, `minor`, `major`), alert level, recommended action, and the release URL when GitHub lookups succeed.
Expand Down
3 changes: 2 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,14 @@ The main operational endpoints are:
- `GET /api/stats`
- `GET /api/recent`
- `GET /api/traces`
- `GET /api/operator-events`
- `GET /dashboard`

`/health` now exposes both provider-level health and top-level capability coverage, so operators can quickly see whether the gateway currently has healthy support for `chat`, `image_generation`, `image_editing`, or other boolean capabilities exposed by loaded providers.

`/api/providers` exposes the normalized provider inventory with optional `capability` and `healthy` filters. This is the inventory surface the dashboard should use when it needs provider metadata beyond raw request metrics.

`/api/stats`, `/api/recent`, and `/api/traces` can now be filtered by provider, client profile, client tag, layer, and success state. The dashboard is a thin UI over those same filtered endpoints and persists its active filters in the URL so operators can share one filtered view.
`/api/stats`, `/api/recent`, and `/api/traces` can now be filtered by provider, client profile, client tag, layer, and success state. `/api/operator-events` captures operator-side update checks and helper-driven apply attempts. The dashboard is a thin UI over those same filtered endpoints and persists its active filters in the URL so operators can share one filtered view.

## Design target

Expand Down
24 changes: 23 additions & 1 deletion foundrygate/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1487,6 +1487,13 @@ def main():
</tr></thead><tbody></tbody></table>
</div>

<div class="sect">
<h2>Operator Actions</h2>
<table id="operators"><thead><tr>
<th>Event</th><th>Action</th><th>Client</th><th>Status</th><th>Update Type</th><th>Eligible</th><th>Events</th>
</tr></thead><tbody></tbody></table>
</div>

<div class="sect">
<h2>Route Traces</h2>
<table id="traces"><thead><tr>
Expand Down Expand Up @@ -1589,13 +1596,14 @@ def main():
persistFilters(query);
const queryStr = query.toString();
const suffix = queryStr ? `?${queryStr}` : '';
const [health, stats, traces, rec, update, inventory] = await Promise.all([
const [health, stats, traces, rec, update, inventory, operatorEvents] = await Promise.all([
fetch('/health').then(r=>r.json()),
fetch(`/api/stats${suffix}`).then(r=>r.json()),
fetch(`/api/traces${suffix}${suffix ? '&' : '?'}limit=20`).then(r=>r.json()),
fetch(`/api/recent${suffix}${suffix ? '&' : '?'}limit=20`).then(r=>r.json()),
fetch('/api/update').then(r=>r.json()).catch(() => ({enabled:false,status:'unavailable'})),
fetch('/api/providers').then(r=>r.json()),
fetch('/api/operator-events?limit=20').then(r=>r.json()).catch(() => ({events: []})),
]);

const totals = stats.totals || {};
Expand All @@ -1609,6 +1617,8 @@ def main():
$('#status').style.background = '#5e5';
$('#ago').textContent = ago(totals.last_request);

const operatorRows = stats.operator_actions || [];
const latestOperatorEvent = (operatorEvents.events || [])[0] || null;
$('#cards').innerHTML = `
<div class="card"><div class="label">Requests</div><div class="value">${fmtTok(totals.total_requests || 0)}</div></div>
<div class="card"><div class="label">Cost</div><div class="value cost">${fmtUsd(totals.total_cost_usd || 0)}</div></div>
Expand All @@ -1620,6 +1630,7 @@ def main():
<div class="card"><div class="label">Capability Coverage</div><div class="value">${coverageEntries.length}</div><div class="detail">${coverageEntries.map(([name]) => name).slice(0,3).join(', ') || 'none'}</div></div>
<div class="card"><div class="label">Top Modality</div><div class="value">${esc(topModality)}</div><div class="detail">${modalityRows.length} modality groups</div></div>
<div class="card"><div class="label">Release Status</div><div class="value ${(update.alert_level === 'critical' || update.alert_level === 'warning') ? 'err' : update.update_available ? 'cost' : ''}">${esc(update.latest_version || update.current_version || 'n/a')}</div><div class="detail">${update.enabled ? (update.status === 'ok' ? `${esc(update.update_type || 'current')} / ${esc(update.recommended_action || (update.update_available ? 'Upgrade recommended' : 'No action needed'))}${update.auto_update && update.auto_update.enabled ? ` / auto: ${esc(update.auto_update.eligible ? 'eligible' : (update.auto_update.blocked_reason || 'blocked'))}` : ''}` : esc(update.recommended_action || 'Update check unavailable')) : 'Update checks disabled'}</div></div>
<div class="card"><div class="label">Operator Actions</div><div class="value">${fmtTok((operatorEvents.events || []).length)}</div><div class="detail">${latestOperatorEvent ? `${esc(latestOperatorEvent.action || 'update-check')} / ${esc(latestOperatorEvent.status || 'unknown')}` : 'No recent operator events'}</div></div>
`;

const providerRows = providers.map(provider => `<tr>
Expand Down Expand Up @@ -1677,6 +1688,17 @@ def main():
</tr>`);
$('#routing tbody').innerHTML = routingRows.length ? routingRows.join('') : emptyRow(6, 'No routing rows for the current filter set');

const operatorBreakdownRows = operatorRows.map(row => `<tr>
<td><span class="pill">${esc(row.event_type || 'update')}</span></td>
<td>${esc(row.action || 'update-check')}</td>
<td>${esc(row.client_tag || 'operator')}</td>
<td>${esc(row.status || 'unknown')}</td>
<td>${esc(row.update_type || '—')}</td>
<td>${row.eligible ? '<span class="tag tag-healthy">yes</span>' : '<span class="tag tag-unhealthy">no</span>'}</td>
<td>${row.events}</td>
</tr>`);
$('#operators tbody').innerHTML = operatorBreakdownRows.length ? operatorBreakdownRows.join('') : emptyRow(7, 'No operator events recorded yet');

const traceRows = (traces.traces || []).map(row => `<tr>
<td class="mono">${ago(row.timestamp)}</td>
<td>${esc(row.provider)}</td>
Expand Down
Loading