Best use case
Skill: Status Page is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
## Overview
Teams using Skill: Status Page should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/status-page/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How Skill: Status Page Compares
| Feature / Agent | Skill: Status Page | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
## Overview
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Skill: Status Page
## Overview
Core domain skill for the self-hosted status page. Covers the public status API, overall status computation, uptime bar generation, incident lifecycle, and the admin dashboard data model.
## Overall Status Computation
```ts
type ServiceStatus = 'up' | 'down' | 'timeout' | 'error';
type OverallStatus = 'operational' | 'degraded' | 'outage' | 'maintenance';
function computeOverallStatus(services: Array<{ status: ServiceStatus }>): OverallStatus {
if (services.length === 0) return 'operational';
const statuses = services.map(s => s.status);
if (statuses.every(s => s === 'up')) return 'operational';
if (statuses.every(s => s !== 'up')) return 'outage';
return 'degraded';
}
```
## Uptime Calculation
```ts
// api/src/lib/uptime.ts
import type { Database } from 'better-sqlite3';
export function calculateUptime(db: Database, serviceId: number, days: number): number {
const row = db.prepare(`
SELECT
COUNT(*) AS total,
SUM(CASE WHEN status = 'up' THEN 1 ELSE 0 END) AS up_count
FROM checks
WHERE service_id = ?
AND checked_at >= datetime('now', '-' || ? || ' days')
`).get(serviceId, days) as { total: number; up_count: number } | undefined;
if (!row || row.total === 0) return 100; // no data = assume operational
return Math.round((row.up_count / row.total) * 1000) / 10; // one decimal place
}
```
## Uptime Bar Generation
```ts
export function buildUptimeBar(
db: Database,
serviceId: number,
days: number,
): string[] {
const rows = db.prepare(`
SELECT
date(checked_at) AS day,
SUM(CASE WHEN status = 'up' THEN 1 ELSE 0 END) AS up_count,
SUM(CASE WHEN status = 'down' THEN 1 ELSE 0 END) AS down_count,
SUM(CASE WHEN status IN ('timeout','error') THEN 1 ELSE 0 END) AS warn_count,
COUNT(*) AS total
FROM checks
WHERE service_id = ?
AND checked_at >= datetime('now', '-' || ? || ' days')
GROUP BY date(checked_at)
ORDER BY day ASC
`).all(serviceId, days) as Array<{
day: string; up_count: number; down_count: number; warn_count: number; total: number;
}>;
const byDay = new Map(rows.map(r => [r.day, r]));
const result: string[] = [];
for (let i = days - 1; i >= 0; i--) {
const d = new Date();
d.setDate(d.getDate() - i);
const key = d.toISOString().slice(0, 10);
const row = byDay.get(key);
if (!row) {
result.push('no-data');
} else if (row.down_count > 0) {
result.push('down');
} else if (row.warn_count > row.up_count) {
result.push('timeout');
} else {
result.push('up');
}
}
return result;
}
```
## Public Status Endpoint
```ts
// api/src/routes/public.ts
router.get('/status', (req, res) => {
const services = db.prepare(`
SELECT s.*,
(SELECT status FROM checks WHERE service_id = s.id ORDER BY checked_at DESC LIMIT 1) AS current_status,
(SELECT response_ms FROM checks WHERE service_id = s.id ORDER BY checked_at DESC LIMIT 1) AS last_response_ms,
(SELECT checked_at FROM checks WHERE service_id = s.id ORDER BY checked_at DESC LIMIT 1) AS last_checked_at
FROM services s
WHERE s.is_active = 1
ORDER BY s.display_order ASC, s.id ASC
`).all() as ServiceRow[];
const siteName = db.prepare(`SELECT value FROM site_settings WHERE key = 'site_name'`).get() as { value: string } | undefined;
const result = services.map(s => ({
id: s.id,
name: s.name,
description: s.description,
status: s.current_status ?? 'no-data',
uptime30d: calculateUptime(db, s.id, 30),
uptimeBar: buildUptimeBar(db, s.id, 90),
lastCheck: s.last_checked_at ? {
status: s.current_status,
responseMs: s.last_response_ms,
checkedAt: s.last_checked_at,
} : null,
}));
res.json({
siteName: siteName?.value ?? 'Status',
overallStatus: computeOverallStatus(result),
services: result,
activeIncidents: getActiveIncidents(db),
});
});
```
## Auto-Incident Logic
```ts
// api/src/lib/checker.ts (excerpt)
function maybeOpenAutoIncident(db: Database, serviceId: number): void {
const recent = db.prepare(`
SELECT status FROM checks
WHERE service_id = ?
ORDER BY checked_at DESC
LIMIT 3
`).all(serviceId) as Array<{ status: string }>;
if (recent.length < 3) return;
if (recent.some(r => r.status === 'up')) return;
// Check no open incident already exists
const open = db.prepare(`
SELECT id FROM incidents
WHERE service_id = ? AND resolved_at IS NULL
`).get(serviceId);
if (open) return;
const svc = db.prepare('SELECT name FROM services WHERE id = ?').get(serviceId) as { name: string };
const incident = db.prepare(`
INSERT INTO incidents (service_id, title, status, impact, auto_opened)
VALUES (?, ?, 'investigating', 'minor', 1)
RETURNING *
`).get(serviceId, `${svc.name} is down`) as { id: number };
db.prepare(`
INSERT INTO incident_updates (incident_id, message, status)
VALUES (?, ?, 'investigating')
`).run(incident.id, 'Automatic incident opened: 3 consecutive failed health checks.');
}
```
## Incident Status Transitions
Valid transitions:
- `investigating` -> `identified` | `monitoring` | `resolved`
- `identified` -> `monitoring` | `resolved`
- `monitoring` -> `resolved` | `identified`
- `resolved` is final
On posting an update with `status = 'resolved'`, set `resolved_at = NOW()` on the parent incident:
```ts
router.post('/:id/updates', requireAuth, (req, res) => {
const { message, status } = req.body as { message: string; status: string };
db.prepare(`
INSERT INTO incident_updates (incident_id, message, status)
VALUES (?, ?, ?)
`).run(req.params.id, message, status);
if (status === 'resolved') {
db.prepare(`
UPDATE incidents SET status = 'resolved', resolved_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
WHERE id = ?
`).run(req.params.id);
} else {
db.prepare(`UPDATE incidents SET status = ? WHERE id = ?`).run(status, req.params.id);
}
res.json({ ok: true });
});
```
## TypeScript Types
```ts
// api/src/types.ts
export interface Service {
id: number;
name: string;
url: string;
description: string | null;
check_interval: number;
timeout_seconds: number;
expected_status: number;
auto_incident: number;
is_active: number;
display_order: number;
created_at: string;
}
export interface Check {
id: number;
service_id: number;
checked_at: string;
status: 'up' | 'down' | 'timeout' | 'error';
status_code: number | null;
response_ms: number | null;
error_message: string | null;
}
export interface Incident {
id: number;
service_id: number | null;
title: string;
status: 'investigating' | 'identified' | 'monitoring' | 'resolved';
impact: 'none' | 'minor' | 'major' | 'critical';
started_at: string;
resolved_at: string | null;
auto_opened: number;
}
export interface IncidentUpdate {
id: number;
incident_id: number;
message: string;
status: 'investigating' | 'identified' | 'monitoring' | 'resolved';
posted_at: string;
}
```
## UptimeBar React Component
```tsx
// web/src/components/UptimeBar.tsx
const STATUS_COLORS: Record<string, string> = {
up: 'var(--uptime-seg-up)',
down: 'var(--uptime-seg-down)',
timeout: 'var(--uptime-seg-timeout)',
error: 'var(--uptime-seg-timeout)',
'no-data': 'var(--uptime-seg-no-data)',
};
export function UptimeBar({ segments }: { segments: string[] }) {
return (
<div style={{ display: 'flex', gap: 2, height: 24 }}>
{segments.map((s, i) => (
<div
key={i}
style={{
flex: 1,
minWidth: 3,
maxWidth: 6,
borderRadius: 2,
background: STATUS_COLORS[s] ?? STATUS_COLORS['no-data'],
}}
title={`Day ${segments.length - i}: ${s}`}
aria-label={`Day ${segments.length - i}: ${s}`}
/>
))}
</div>
);
}
```
## pnpm Commands
```bash
pnpm install # install all workspace deps
pnpm --filter api dev # run API with ts-node-dev (port 3001)
pnpm --filter web dev # Vite dev server (port 5173)
pnpm --filter api test # Vitest
pnpm --filter api build # compile TS
pnpm --filter web build # Vite build
pnpm typecheck # tsc --noEmit in all packages
pnpm lint # ESLint
```Related Skills
Skill: Uptime Monitoring
## Overview
Skill: unit-conversion
## Overview
Skill: recipe-scaler
## Overview
reading-list
Operate the reading-list API to save, manage, tag, search, and export articles.
email-digest
Configure, test, and troubleshoot the reading-list daily email digest delivered via nodemailer.
websocket-realtime
Use the WebSocket connection in poll-builder to receive live vote updates. Use when you need to stream real-time poll results, monitor a poll for new votes, or build a live dashboard. Triggers include "live results", "real-time updates", "stream votes", "watch poll", or "WebSocket".
poll-builder
Self-hosted poll creation tool with real-time results. Use when you need to create a poll, check vote counts, close a poll, export results, or get the shareable link for a poll. Triggers include "create poll", "vote", "poll results", "survey", "collect votes", "share poll", or any task involving polling or voting.
Skill: personal-finance
## Overview
Skill: csv-import
## Overview
Skill: Syntax Highlighting
## Purpose
Skill: Pastebin Core
## Purpose
Skill: Cost Reporting
## Overview