```
CSS:
```css
.filter-btn {
background: rgba(15,23,42,0.4);
border: 1px solid var(--border-card);
color: var(--text-secondary);
padding: 4px 12px;
border-radius: 6px;
font-size: 0.78rem;
cursor: pointer;
}
.filter-btn.active {
border-color: var(--accent);
color: var(--accent);
background: rgba(245,158,11,0.08);
}
.filter-btn .fb-count { margin-left: 4px; opacity: 0.7; }
```
JS pattern: maintain `currentFilter` and `failureItems[]`. Re-filter on each `applyFilter()` call by iterating items and checking status against the active filter. Toggle active class on buttons by matching `data-filter` attribute.
### Multi-source failure aggregation
The `collectFailures(data)` function walks ALL data sections and collects non-OK items into a flat array tagged with `source`:
```javascript
function collectFailures(data) {
var items = [];
// API checks
var apiChecks = getNested(data, 'api_checks', []);
if (Array.isArray(apiChecks)) {
for (var i = 0; i < apiChecks.length; i++) {
items.push({ source: 'API Check', item: c.endpoint, status: cleanStatus, statusLabel: originalStatus, detail: c.message });
}
}
// Services — only non-active
// Cron jobs — only error/failed
// S3 backups — only error/failed
// API health — only error/failed
return items;
}
```
Then OK items are added separately. The combined list drives both the filter counts and the table rendering.
### Red/amber card variants
- **Cron errors** use `class="card card-red"` with `--red-border` and `--red-bg`
- **Future notes** use `class="card card-amber"` with `rgba(245,158,11,0.3)` border and `--amber-bg`
```css
.card-red {
border-color: rgba(239,68,68,0.4);
background: var(--red-bg);
}
.card-amber {
border-color: rgba(245,158,11,0.3);
background: var(--amber-bg);
}
```
## Data flow notes
- The page fetches `/data/ops-status.json` on load and auto-refreshes every 30s
- Falls back gracefully: `api_checks` → `api_health` if primary field missing
- Cron errors section is conditionally shown/hidden via `display: none` — the red card is gated on `errors.length > 0`
- All sections have empty states that say "No X data available" when their source array is empty
## nav.html integration
The page uses the shared nav partial via XHR:
```html
```