# Ops Portal — Logs & Events Page Pattern Created: 2026-07-08 Page: `/var/www/ops/logs.html` Source data: `/data/ops-status.json` ## Page Sections | Section | Data Source | Render Pattern | |---------|-------------|----------------| | **Recent API Health Events** | `data.api_checks` (array) | Vertical timeline with color-coded dots per endpoint. Falls back to `data.api_health` if `api_checks` absent. | | **Service Events** | `data.services` (array) | Card grid. Cards with non-`active` status get red border/background. | | **Cron Job Errors** | `data.scheduled_jobs` (array) | Prominent red card section (`card-red`), hidden when no errors. Shows job name, last run (with relative age), script path, link to `/cron.html`. Green empty-state when clean. | | **Recent Failures Summary** | Aggregated from API checks, services, cron jobs, S3 | Filter-bar table (All / Errors / Warnings / OK) with per-filter counts. JS-based client-side filtering. | | **Future / Notes** | Static | Amber card listing planned syslog/journald integration | ## Reusable Patterns ### Timeline component A vertical timeline with a left-side dot-and-line track. Each item has a colored dot (green/red/amber/gray), endpoint name, status badge, and timestamp with relative age. ```html
api.example.com/health OK Jul 8, 10:45 PM (2m ago)
``` CSS: ```css .timeline { position: relative; padding-left: 24px; } .timeline::before { content: ''; position: absolute; left: 7px; top: 4px; bottom: 4px; width: 2px; background: var(--border-card); } .timeline-item { position: relative; padding: 8px 0 8px 12px; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; border-bottom: 1px solid rgba(51,65,85,0.3); } .timeline-item .tl-dot { position: absolute; left: -20px; top: 12px; width: 10px; height: 10px; border-radius: 50%; } .tl-dot.tl-ok { background: var(--green); } .tl-dot.tl-error { background: var(--red); } .tl-dot.tl-amber { background: var(--amber); } .tl-dot.tl-gray { background: var(--gray); } ``` ### Filter bar with counts Bootstrap-style filter pills that show item counts and toggle content visibility: ```html
``` 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 ```