Initial skills documentation — 25 categories, all SKILL.md + references + scripts
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
# 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
|
||||
<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:
|
||||
```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
|
||||
<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:
|
||||
```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
|
||||
<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>
|
||||
```
|
||||
Reference in New Issue
Block a user