Files
hermes-skills/skills/software-development/portal-ui-mockups/references/logs-events-page-pattern.md
T

5.5 KiB

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.

<div class="timeline">
  <div class="timeline-item">
    <span class="tl-dot tl-ok"></span>
    <span class="tl-endpoint">api.example.com/health</span>
    <span class="tl-status"><span class="badge badge-ok">OK</span></span>
    <span class="tl-time">Jul 8, 10:45 PM (2m ago)</span>
  </div>
</div>

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:

<div class="filter-bar">
  <button class="filter-btn active" data-filter="all" onclick="applyFilter('all')">All <span class="fb-count" id="fAll">0</span></button>
  <button class="filter-btn" data-filter="error" onclick="applyFilter('error')">Errors <span class="fb-count" id="fError">0</span></button>
  <button class="filter-btn" data-filter="ok" onclick="applyFilter('ok')">OK <span class="fb-count" id="fOk">0</span></button>
</div>

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:

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
.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_checksapi_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:

<div id="navPlaceholder"></div>
<script>
function loadNav() {
  var xhr = new XMLHttpRequest();
  xhr.open('GET', '/nav.html', true);
  xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
      document.getElementById('navPlaceholder').innerHTML = xhr.responseText;
    }
  };
  xhr.send();
}
document.addEventListener('DOMContentLoaded', function() { loadNav(); loadLogs(); });
</script>