tlc-claude-code 1.4.8 → 1.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server/index.js +229 -14
- package/server/lib/compliance/control-mapper.js +401 -0
- package/server/lib/compliance/control-mapper.test.js +117 -0
- package/server/lib/compliance/evidence-linker.js +296 -0
- package/server/lib/compliance/evidence-linker.test.js +121 -0
- package/server/lib/compliance/gdpr-checklist.js +416 -0
- package/server/lib/compliance/gdpr-checklist.test.js +131 -0
- package/server/lib/compliance/hipaa-checklist.js +277 -0
- package/server/lib/compliance/hipaa-checklist.test.js +101 -0
- package/server/lib/compliance/iso27001-checklist.js +287 -0
- package/server/lib/compliance/iso27001-checklist.test.js +99 -0
- package/server/lib/compliance/multi-framework-reporter.js +284 -0
- package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
- package/server/lib/compliance/pci-dss-checklist.js +214 -0
- package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
- package/server/lib/compliance/trust-centre.js +187 -0
- package/server/lib/compliance/trust-centre.test.js +93 -0
- package/server/lib/dashboard/api-server.js +155 -0
- package/server/lib/dashboard/api-server.test.js +155 -0
- package/server/lib/dashboard/health-api.js +199 -0
- package/server/lib/dashboard/health-api.test.js +122 -0
- package/server/lib/dashboard/notes-api.js +234 -0
- package/server/lib/dashboard/notes-api.test.js +134 -0
- package/server/lib/dashboard/router-api.js +176 -0
- package/server/lib/dashboard/router-api.test.js +132 -0
- package/server/lib/dashboard/tasks-api.js +289 -0
- package/server/lib/dashboard/tasks-api.test.js +161 -0
- package/server/lib/dashboard/tlc-introspection.js +197 -0
- package/server/lib/dashboard/tlc-introspection.test.js +138 -0
- package/server/lib/dashboard/version-api.js +222 -0
- package/server/lib/dashboard/version-api.test.js +112 -0
- package/server/lib/dashboard/websocket-server.js +104 -0
- package/server/lib/dashboard/websocket-server.test.js +118 -0
- package/server/lib/deploy/branch-classifier.js +163 -0
- package/server/lib/deploy/branch-classifier.test.js +164 -0
- package/server/lib/deploy/deployment-approval.js +299 -0
- package/server/lib/deploy/deployment-approval.test.js +296 -0
- package/server/lib/deploy/deployment-audit.js +374 -0
- package/server/lib/deploy/deployment-audit.test.js +307 -0
- package/server/lib/deploy/deployment-executor.js +335 -0
- package/server/lib/deploy/deployment-executor.test.js +329 -0
- package/server/lib/deploy/deployment-rules.js +163 -0
- package/server/lib/deploy/deployment-rules.test.js +188 -0
- package/server/lib/deploy/rollback-manager.js +379 -0
- package/server/lib/deploy/rollback-manager.test.js +321 -0
- package/server/lib/deploy/security-gates.js +236 -0
- package/server/lib/deploy/security-gates.test.js +222 -0
- package/server/lib/k8s/gitops-config.js +188 -0
- package/server/lib/k8s/gitops-config.test.js +59 -0
- package/server/lib/k8s/helm-generator.js +196 -0
- package/server/lib/k8s/helm-generator.test.js +59 -0
- package/server/lib/k8s/kustomize-generator.js +176 -0
- package/server/lib/k8s/kustomize-generator.test.js +58 -0
- package/server/lib/k8s/network-policy.js +114 -0
- package/server/lib/k8s/network-policy.test.js +53 -0
- package/server/lib/k8s/pod-security.js +114 -0
- package/server/lib/k8s/pod-security.test.js +55 -0
- package/server/lib/k8s/rbac-generator.js +132 -0
- package/server/lib/k8s/rbac-generator.test.js +57 -0
- package/server/lib/k8s/resource-manager.js +172 -0
- package/server/lib/k8s/resource-manager.test.js +60 -0
- package/server/lib/k8s/secrets-encryption.js +168 -0
- package/server/lib/k8s/secrets-encryption.test.js +49 -0
- package/server/lib/monitoring/alert-manager.js +238 -0
- package/server/lib/monitoring/alert-manager.test.js +106 -0
- package/server/lib/monitoring/health-check.js +226 -0
- package/server/lib/monitoring/health-check.test.js +176 -0
- package/server/lib/monitoring/incident-manager.js +230 -0
- package/server/lib/monitoring/incident-manager.test.js +98 -0
- package/server/lib/monitoring/log-aggregator.js +147 -0
- package/server/lib/monitoring/log-aggregator.test.js +89 -0
- package/server/lib/monitoring/metrics-collector.js +337 -0
- package/server/lib/monitoring/metrics-collector.test.js +172 -0
- package/server/lib/monitoring/status-page.js +214 -0
- package/server/lib/monitoring/status-page.test.js +105 -0
- package/server/lib/monitoring/uptime-monitor.js +194 -0
- package/server/lib/monitoring/uptime-monitor.test.js +109 -0
- package/server/lib/network/fail2ban-config.js +294 -0
- package/server/lib/network/fail2ban-config.test.js +275 -0
- package/server/lib/network/firewall-manager.js +252 -0
- package/server/lib/network/firewall-manager.test.js +254 -0
- package/server/lib/network/geoip-filter.js +282 -0
- package/server/lib/network/geoip-filter.test.js +264 -0
- package/server/lib/network/rate-limiter.js +229 -0
- package/server/lib/network/rate-limiter.test.js +293 -0
- package/server/lib/network/request-validator.js +351 -0
- package/server/lib/network/request-validator.test.js +345 -0
- package/server/lib/network/security-headers.js +251 -0
- package/server/lib/network/security-headers.test.js +283 -0
- package/server/lib/network/tls-config.js +210 -0
- package/server/lib/network/tls-config.test.js +248 -0
- package/server/lib/security/auth-security.js +369 -0
- package/server/lib/security/auth-security.test.js +448 -0
- package/server/lib/security/cis-benchmark.js +152 -0
- package/server/lib/security/cis-benchmark.test.js +137 -0
- package/server/lib/security/compose-templates.js +312 -0
- package/server/lib/security/compose-templates.test.js +229 -0
- package/server/lib/security/container-runtime.js +456 -0
- package/server/lib/security/container-runtime.test.js +503 -0
- package/server/lib/security/cors-validator.js +278 -0
- package/server/lib/security/cors-validator.test.js +310 -0
- package/server/lib/security/crypto-utils.js +253 -0
- package/server/lib/security/crypto-utils.test.js +409 -0
- package/server/lib/security/dockerfile-linter.js +459 -0
- package/server/lib/security/dockerfile-linter.test.js +483 -0
- package/server/lib/security/dockerfile-templates.js +278 -0
- package/server/lib/security/dockerfile-templates.test.js +164 -0
- package/server/lib/security/error-sanitizer.js +426 -0
- package/server/lib/security/error-sanitizer.test.js +331 -0
- package/server/lib/security/headers-generator.js +368 -0
- package/server/lib/security/headers-generator.test.js +398 -0
- package/server/lib/security/image-scanner.js +83 -0
- package/server/lib/security/image-scanner.test.js +106 -0
- package/server/lib/security/input-validator.js +352 -0
- package/server/lib/security/input-validator.test.js +330 -0
- package/server/lib/security/network-policy.js +174 -0
- package/server/lib/security/network-policy.test.js +164 -0
- package/server/lib/security/output-encoder.js +237 -0
- package/server/lib/security/output-encoder.test.js +276 -0
- package/server/lib/security/path-validator.js +359 -0
- package/server/lib/security/path-validator.test.js +293 -0
- package/server/lib/security/query-builder.js +421 -0
- package/server/lib/security/query-builder.test.js +318 -0
- package/server/lib/security/secret-detector.js +290 -0
- package/server/lib/security/secret-detector.test.js +354 -0
- package/server/lib/security/secrets-validator.js +137 -0
- package/server/lib/security/secrets-validator.test.js +120 -0
- package/server/lib/security-testing/dast-runner.js +154 -0
- package/server/lib/security-testing/dast-runner.test.js +62 -0
- package/server/lib/security-testing/dependency-scanner.js +172 -0
- package/server/lib/security-testing/dependency-scanner.test.js +64 -0
- package/server/lib/security-testing/pentest-runner.js +230 -0
- package/server/lib/security-testing/pentest-runner.test.js +60 -0
- package/server/lib/security-testing/sast-runner.js +136 -0
- package/server/lib/security-testing/sast-runner.test.js +62 -0
- package/server/lib/security-testing/secret-scanner.js +153 -0
- package/server/lib/security-testing/secret-scanner.test.js +66 -0
- package/server/lib/security-testing/security-gate.js +216 -0
- package/server/lib/security-testing/security-gate.test.js +115 -0
- package/server/lib/security-testing/security-reporter.js +303 -0
- package/server/lib/security-testing/security-reporter.test.js +114 -0
- package/server/lib/standards/audit-checker.js +546 -0
- package/server/lib/standards/audit-checker.test.js +415 -0
- package/server/lib/standards/cleanup-executor.js +452 -0
- package/server/lib/standards/cleanup-executor.test.js +293 -0
- package/server/lib/standards/refactor-stepper.js +425 -0
- package/server/lib/standards/refactor-stepper.test.js +298 -0
- package/server/lib/standards/standards-injector.js +167 -0
- package/server/lib/standards/standards-injector.test.js +232 -0
- package/server/lib/user-management.test.js +284 -0
- package/server/lib/vps/backup-manager.js +157 -0
- package/server/lib/vps/backup-manager.test.js +59 -0
- package/server/lib/vps/caddy-config.js +159 -0
- package/server/lib/vps/caddy-config.test.js +48 -0
- package/server/lib/vps/compose-orchestrator.js +219 -0
- package/server/lib/vps/compose-orchestrator.test.js +50 -0
- package/server/lib/vps/database-config.js +208 -0
- package/server/lib/vps/database-config.test.js +47 -0
- package/server/lib/vps/deploy-script.js +211 -0
- package/server/lib/vps/deploy-script.test.js +53 -0
- package/server/lib/vps/secrets-manager.js +148 -0
- package/server/lib/vps/secrets-manager.test.js +58 -0
- package/server/lib/vps/server-hardening.js +174 -0
- package/server/lib/vps/server-hardening.test.js +70 -0
- package/server/package-lock.json +19 -0
- package/server/package.json +1 -0
- package/server/templates/CLAUDE.md +37 -0
- package/server/templates/CODING-STANDARDS.md +408 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics Collector Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
createCounter,
|
|
7
|
+
createHistogram,
|
|
8
|
+
createGauge,
|
|
9
|
+
collectMetrics,
|
|
10
|
+
formatPrometheus,
|
|
11
|
+
METRIC_TYPES,
|
|
12
|
+
createMetricsCollector,
|
|
13
|
+
} from './metrics-collector.js';
|
|
14
|
+
|
|
15
|
+
describe('metrics-collector', () => {
|
|
16
|
+
describe('METRIC_TYPES', () => {
|
|
17
|
+
it('defines metric type constants', () => {
|
|
18
|
+
expect(METRIC_TYPES.COUNTER).toBe('counter');
|
|
19
|
+
expect(METRIC_TYPES.HISTOGRAM).toBe('histogram');
|
|
20
|
+
expect(METRIC_TYPES.GAUGE).toBe('gauge');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('createCounter', () => {
|
|
25
|
+
it('creates counter with initial value 0', () => {
|
|
26
|
+
const counter = createCounter('http_requests_total');
|
|
27
|
+
expect(counter.get()).toBe(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('increments counter', () => {
|
|
31
|
+
const counter = createCounter('http_requests_total');
|
|
32
|
+
counter.inc();
|
|
33
|
+
expect(counter.get()).toBe(1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('increments by specific value', () => {
|
|
37
|
+
const counter = createCounter('http_requests_total');
|
|
38
|
+
counter.inc(5);
|
|
39
|
+
expect(counter.get()).toBe(5);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('supports labels', () => {
|
|
43
|
+
const counter = createCounter('http_requests_total', { labels: ['method', 'status'] });
|
|
44
|
+
counter.inc({ method: 'GET', status: '200' });
|
|
45
|
+
expect(counter.get({ method: 'GET', status: '200' })).toBe(1);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('createHistogram', () => {
|
|
50
|
+
it('records observations', () => {
|
|
51
|
+
const histogram = createHistogram('http_request_duration_seconds');
|
|
52
|
+
histogram.observe(0.5);
|
|
53
|
+
expect(histogram.getCount()).toBe(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('calculates percentiles', () => {
|
|
57
|
+
const histogram = createHistogram('http_request_duration_seconds');
|
|
58
|
+
for (let i = 0; i < 100; i++) histogram.observe(i / 100);
|
|
59
|
+
|
|
60
|
+
expect(histogram.getPercentile(50)).toBeCloseTo(0.5, 1);
|
|
61
|
+
expect(histogram.getPercentile(95)).toBeCloseTo(0.95, 1);
|
|
62
|
+
expect(histogram.getPercentile(99)).toBeCloseTo(0.99, 1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('tracks sum', () => {
|
|
66
|
+
const histogram = createHistogram('http_request_duration_seconds');
|
|
67
|
+
histogram.observe(1);
|
|
68
|
+
histogram.observe(2);
|
|
69
|
+
expect(histogram.getSum()).toBe(3);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('createGauge', () => {
|
|
74
|
+
it('sets value', () => {
|
|
75
|
+
const gauge = createGauge('memory_usage_bytes');
|
|
76
|
+
gauge.set(1024);
|
|
77
|
+
expect(gauge.get()).toBe(1024);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('increments value', () => {
|
|
81
|
+
const gauge = createGauge('active_connections');
|
|
82
|
+
gauge.set(10);
|
|
83
|
+
gauge.inc();
|
|
84
|
+
expect(gauge.get()).toBe(11);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('decrements value', () => {
|
|
88
|
+
const gauge = createGauge('active_connections');
|
|
89
|
+
gauge.set(10);
|
|
90
|
+
gauge.dec();
|
|
91
|
+
expect(gauge.get()).toBe(9);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('collectMetrics', () => {
|
|
96
|
+
it('collects CPU usage', async () => {
|
|
97
|
+
const metrics = await collectMetrics({ cpu: true });
|
|
98
|
+
expect(metrics.cpu).toBeDefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('collects memory usage', async () => {
|
|
102
|
+
const metrics = await collectMetrics({ memory: true });
|
|
103
|
+
expect(metrics.memory).toBeDefined();
|
|
104
|
+
expect(metrics.memory.heapUsed).toBeDefined();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('collects event loop lag', async () => {
|
|
108
|
+
const metrics = await collectMetrics({ eventLoop: true });
|
|
109
|
+
expect(metrics.eventLoop).toBeDefined();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('formatPrometheus', () => {
|
|
114
|
+
it('formats counter metric', () => {
|
|
115
|
+
const output = formatPrometheus({
|
|
116
|
+
name: 'http_requests_total',
|
|
117
|
+
type: 'counter',
|
|
118
|
+
value: 100,
|
|
119
|
+
});
|
|
120
|
+
expect(output).toContain('# TYPE http_requests_total counter');
|
|
121
|
+
expect(output).toContain('http_requests_total 100');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('formats histogram metric', () => {
|
|
125
|
+
const output = formatPrometheus({
|
|
126
|
+
name: 'http_request_duration_seconds',
|
|
127
|
+
type: 'histogram',
|
|
128
|
+
buckets: { 0.1: 10, 0.5: 50, 1: 100 },
|
|
129
|
+
sum: 25,
|
|
130
|
+
count: 100,
|
|
131
|
+
});
|
|
132
|
+
expect(output).toContain('# TYPE http_request_duration_seconds histogram');
|
|
133
|
+
expect(output).toContain('_bucket');
|
|
134
|
+
expect(output).toContain('_sum');
|
|
135
|
+
expect(output).toContain('_count');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('includes labels', () => {
|
|
139
|
+
const output = formatPrometheus({
|
|
140
|
+
name: 'http_requests_total',
|
|
141
|
+
type: 'counter',
|
|
142
|
+
value: 100,
|
|
143
|
+
labels: { method: 'GET', status: '200' },
|
|
144
|
+
});
|
|
145
|
+
expect(output).toContain('{method="GET",status="200"}');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('createMetricsCollector', () => {
|
|
150
|
+
it('creates collector with methods', () => {
|
|
151
|
+
const collector = createMetricsCollector();
|
|
152
|
+
expect(collector.counter).toBeDefined();
|
|
153
|
+
expect(collector.histogram).toBeDefined();
|
|
154
|
+
expect(collector.gauge).toBeDefined();
|
|
155
|
+
expect(collector.collect).toBeDefined();
|
|
156
|
+
expect(collector.format).toBeDefined();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('tracks request metrics', () => {
|
|
160
|
+
const collector = createMetricsCollector();
|
|
161
|
+
collector.trackRequest({ method: 'GET', path: '/api/users', status: 200, duration: 0.1 });
|
|
162
|
+
|
|
163
|
+
const metrics = collector.collect();
|
|
164
|
+
expect(metrics.requests).toBeDefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('configures retention period', () => {
|
|
168
|
+
const collector = createMetricsCollector({ retentionMs: 3600000 });
|
|
169
|
+
expect(collector.getRetention()).toBe(3600000);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status Page Generator
|
|
3
|
+
* Generates HTML status pages and RSS feeds
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Component status constants
|
|
8
|
+
*/
|
|
9
|
+
export const COMPONENT_STATUS = {
|
|
10
|
+
OPERATIONAL: 'operational',
|
|
11
|
+
DEGRADED: 'degraded',
|
|
12
|
+
OUTAGE: 'outage',
|
|
13
|
+
MAINTENANCE: 'maintenance',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Gets component status based on health metrics
|
|
18
|
+
* @param {Object} component - Component health data
|
|
19
|
+
* @param {boolean} component.healthy - Is component healthy
|
|
20
|
+
* @param {number} component.responseTime - Response time in ms
|
|
21
|
+
* @param {number} component.threshold - Response time threshold
|
|
22
|
+
* @returns {string} Component status
|
|
23
|
+
*/
|
|
24
|
+
export function getComponentStatus(component) {
|
|
25
|
+
if (!component.healthy) {
|
|
26
|
+
return COMPONENT_STATUS.OUTAGE;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (component.threshold && component.responseTime > component.threshold) {
|
|
30
|
+
return COMPONENT_STATUS.DEGRADED;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return COMPONENT_STATUS.OPERATIONAL;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Calculates overall status from components
|
|
38
|
+
* @param {Array} components - Component statuses
|
|
39
|
+
* @returns {string} Overall status
|
|
40
|
+
*/
|
|
41
|
+
function getOverallStatus(components) {
|
|
42
|
+
if (components.some(c => c.status === COMPONENT_STATUS.OUTAGE)) {
|
|
43
|
+
return COMPONENT_STATUS.OUTAGE;
|
|
44
|
+
}
|
|
45
|
+
if (components.some(c => c.status === COMPONENT_STATUS.MAINTENANCE)) {
|
|
46
|
+
return COMPONENT_STATUS.MAINTENANCE;
|
|
47
|
+
}
|
|
48
|
+
if (components.some(c => c.status === COMPONENT_STATUS.DEGRADED)) {
|
|
49
|
+
return COMPONENT_STATUS.DEGRADED;
|
|
50
|
+
}
|
|
51
|
+
return COMPONENT_STATUS.OPERATIONAL;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generates an HTML status page
|
|
56
|
+
* @param {Object} options - Page options
|
|
57
|
+
* @param {string} options.title - Page title
|
|
58
|
+
* @param {Array} options.components - Component statuses
|
|
59
|
+
* @param {Array} options.incidents - Recent incidents
|
|
60
|
+
* @returns {string} HTML status page
|
|
61
|
+
*/
|
|
62
|
+
export function generateStatusPage(options) {
|
|
63
|
+
const { title = 'Status Page', components = [], incidents = [] } = options;
|
|
64
|
+
const overall = getOverallStatus(components);
|
|
65
|
+
|
|
66
|
+
const componentHtml = components
|
|
67
|
+
.map(c => `<div class="component ${c.status}"><span class="name">${c.name}</span><span class="status">${c.status}</span></div>`)
|
|
68
|
+
.join('\n');
|
|
69
|
+
|
|
70
|
+
const incidentHtml = incidents.length > 0
|
|
71
|
+
? formatIncidentHistory(incidents)
|
|
72
|
+
: '<p>No recent incidents</p>';
|
|
73
|
+
|
|
74
|
+
return `<!DOCTYPE html>
|
|
75
|
+
<html lang="en">
|
|
76
|
+
<head>
|
|
77
|
+
<meta charset="UTF-8">
|
|
78
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
79
|
+
<title>${title}</title>
|
|
80
|
+
<style>
|
|
81
|
+
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
|
|
82
|
+
.overall { padding: 20px; border-radius: 8px; margin-bottom: 20px; }
|
|
83
|
+
.overall.operational { background: #d4edda; }
|
|
84
|
+
.overall.degraded { background: #fff3cd; }
|
|
85
|
+
.overall.outage { background: #f8d7da; }
|
|
86
|
+
.overall.maintenance { background: #cce5ff; }
|
|
87
|
+
.component { display: flex; justify-content: space-between; padding: 10px; border-bottom: 1px solid #eee; }
|
|
88
|
+
.incident { margin-bottom: 15px; padding: 10px; background: #f8f9fa; border-radius: 4px; }
|
|
89
|
+
</style>
|
|
90
|
+
</head>
|
|
91
|
+
<body>
|
|
92
|
+
<h1>${title}</h1>
|
|
93
|
+
<div class="overall ${overall}">
|
|
94
|
+
<strong>Overall Status:</strong> ${overall}
|
|
95
|
+
</div>
|
|
96
|
+
<h2>Components</h2>
|
|
97
|
+
<div class="components">
|
|
98
|
+
${componentHtml}
|
|
99
|
+
</div>
|
|
100
|
+
<h2>Incident History</h2>
|
|
101
|
+
<div class="incidents">
|
|
102
|
+
${incidentHtml}
|
|
103
|
+
</div>
|
|
104
|
+
</body>
|
|
105
|
+
</html>`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Formats incident history as HTML
|
|
110
|
+
* @param {Array} incidents - Incidents to format
|
|
111
|
+
* @returns {string} HTML incident list
|
|
112
|
+
*/
|
|
113
|
+
export function formatIncidentHistory(incidents) {
|
|
114
|
+
return incidents
|
|
115
|
+
.map(incident => `<div class="incident">
|
|
116
|
+
<h3>${incident.title}</h3>
|
|
117
|
+
<p><strong>Date:</strong> ${incident.date}</p>
|
|
118
|
+
<p><strong>Status:</strong> ${incident.status}</p>
|
|
119
|
+
</div>`)
|
|
120
|
+
.join('\n');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Generates an RSS feed for status updates
|
|
125
|
+
* @param {Object} options - Feed options
|
|
126
|
+
* @param {string} options.title - Feed title
|
|
127
|
+
* @param {Array} options.incidents - Incidents to include
|
|
128
|
+
* @returns {string} RSS XML
|
|
129
|
+
*/
|
|
130
|
+
export function generateRssFeed(options) {
|
|
131
|
+
const { title = 'Status Updates', incidents = [] } = options;
|
|
132
|
+
|
|
133
|
+
const items = incidents
|
|
134
|
+
.map(incident => ` <item>
|
|
135
|
+
<title>${incident.title}</title>
|
|
136
|
+
<pubDate>${incident.date}</pubDate>
|
|
137
|
+
</item>`)
|
|
138
|
+
.join('\n');
|
|
139
|
+
|
|
140
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
141
|
+
<rss version="2.0">
|
|
142
|
+
<channel>
|
|
143
|
+
<title>${title}</title>
|
|
144
|
+
<description>Status updates and incidents</description>
|
|
145
|
+
${items}
|
|
146
|
+
</channel>
|
|
147
|
+
</rss>`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Creates a status page generator instance
|
|
152
|
+
* @returns {Object} Generator with methods
|
|
153
|
+
*/
|
|
154
|
+
export function createStatusPageGenerator() {
|
|
155
|
+
const components = [];
|
|
156
|
+
const incidents = [];
|
|
157
|
+
const maintenanceSchedule = [];
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
/**
|
|
161
|
+
* Adds a component to track
|
|
162
|
+
* @param {Object} component - Component configuration
|
|
163
|
+
*/
|
|
164
|
+
addComponent(component) {
|
|
165
|
+
components.push(component);
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Adds an incident
|
|
170
|
+
* @param {Object} incident - Incident to add
|
|
171
|
+
*/
|
|
172
|
+
addIncident(incident) {
|
|
173
|
+
incidents.push(incident);
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Schedules maintenance for a component
|
|
178
|
+
* @param {Object} maintenance - Maintenance schedule
|
|
179
|
+
*/
|
|
180
|
+
scheduleMaintenance(maintenance) {
|
|
181
|
+
maintenanceSchedule.push(maintenance);
|
|
182
|
+
|
|
183
|
+
// Find component and update its status
|
|
184
|
+
const componentIndex = components.findIndex(c => c.name === maintenance.component);
|
|
185
|
+
if (componentIndex >= 0) {
|
|
186
|
+
components[componentIndex].status = COMPONENT_STATUS.MAINTENANCE;
|
|
187
|
+
} else {
|
|
188
|
+
components.push({
|
|
189
|
+
name: maintenance.component,
|
|
190
|
+
status: COMPONENT_STATUS.MAINTENANCE,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Generates the status page HTML
|
|
197
|
+
* @returns {string} HTML status page
|
|
198
|
+
*/
|
|
199
|
+
generate() {
|
|
200
|
+
return generateStatusPage({
|
|
201
|
+
components,
|
|
202
|
+
incidents,
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Gets RSS feed
|
|
208
|
+
* @returns {string} RSS XML
|
|
209
|
+
*/
|
|
210
|
+
getRss() {
|
|
211
|
+
return generateRssFeed({ incidents });
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status Page Generator Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
generateStatusPage,
|
|
7
|
+
getComponentStatus,
|
|
8
|
+
formatIncidentHistory,
|
|
9
|
+
generateRssFeed,
|
|
10
|
+
COMPONENT_STATUS,
|
|
11
|
+
createStatusPageGenerator,
|
|
12
|
+
} from './status-page.js';
|
|
13
|
+
|
|
14
|
+
describe('status-page', () => {
|
|
15
|
+
describe('COMPONENT_STATUS', () => {
|
|
16
|
+
it('defines status constants', () => {
|
|
17
|
+
expect(COMPONENT_STATUS.OPERATIONAL).toBe('operational');
|
|
18
|
+
expect(COMPONENT_STATUS.DEGRADED).toBe('degraded');
|
|
19
|
+
expect(COMPONENT_STATUS.OUTAGE).toBe('outage');
|
|
20
|
+
expect(COMPONENT_STATUS.MAINTENANCE).toBe('maintenance');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('generateStatusPage', () => {
|
|
25
|
+
it('generates HTML status page', () => {
|
|
26
|
+
const html = generateStatusPage({
|
|
27
|
+
title: 'Service Status',
|
|
28
|
+
components: [{ name: 'API', status: 'operational' }],
|
|
29
|
+
});
|
|
30
|
+
expect(html).toContain('<html');
|
|
31
|
+
expect(html).toContain('Service Status');
|
|
32
|
+
expect(html).toContain('API');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('shows overall status', () => {
|
|
36
|
+
const html = generateStatusPage({
|
|
37
|
+
components: [
|
|
38
|
+
{ name: 'API', status: 'operational' },
|
|
39
|
+
{ name: 'DB', status: 'degraded' },
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
expect(html).toContain('degraded');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('getComponentStatus', () => {
|
|
47
|
+
it('returns operational for healthy component', () => {
|
|
48
|
+
const status = getComponentStatus({ healthy: true, responseTime: 100 });
|
|
49
|
+
expect(status).toBe('operational');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('returns degraded for slow component', () => {
|
|
53
|
+
const status = getComponentStatus({ healthy: true, responseTime: 5000, threshold: 1000 });
|
|
54
|
+
expect(status).toBe('degraded');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns outage for unhealthy component', () => {
|
|
58
|
+
const status = getComponentStatus({ healthy: false });
|
|
59
|
+
expect(status).toBe('outage');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('formatIncidentHistory', () => {
|
|
64
|
+
it('formats incident list', () => {
|
|
65
|
+
const html = formatIncidentHistory([
|
|
66
|
+
{ title: 'API Outage', date: '2024-01-01', status: 'resolved' },
|
|
67
|
+
]);
|
|
68
|
+
expect(html).toContain('API Outage');
|
|
69
|
+
expect(html).toContain('resolved');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('generateRssFeed', () => {
|
|
74
|
+
it('generates RSS feed', () => {
|
|
75
|
+
const rss = generateRssFeed({
|
|
76
|
+
title: 'Status Updates',
|
|
77
|
+
incidents: [{ title: 'Outage', date: '2024-01-01' }],
|
|
78
|
+
});
|
|
79
|
+
expect(rss).toContain('<?xml');
|
|
80
|
+
expect(rss).toContain('<rss');
|
|
81
|
+
expect(rss).toContain('Outage');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('createStatusPageGenerator', () => {
|
|
86
|
+
it('creates generator with methods', () => {
|
|
87
|
+
const generator = createStatusPageGenerator();
|
|
88
|
+
expect(generator.generate).toBeDefined();
|
|
89
|
+
expect(generator.addComponent).toBeDefined();
|
|
90
|
+
expect(generator.addIncident).toBeDefined();
|
|
91
|
+
expect(generator.getRss).toBeDefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('supports scheduled maintenance', () => {
|
|
95
|
+
const generator = createStatusPageGenerator();
|
|
96
|
+
generator.scheduleMaintenance({
|
|
97
|
+
component: 'API',
|
|
98
|
+
start: new Date(),
|
|
99
|
+
end: new Date(Date.now() + 3600000),
|
|
100
|
+
});
|
|
101
|
+
const page = generator.generate();
|
|
102
|
+
expect(page).toContain('maintenance');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uptime Monitor
|
|
3
|
+
* Uptime monitoring and alerting
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const UPTIME_STATUS = {
|
|
7
|
+
UP: 'up',
|
|
8
|
+
DOWN: 'down',
|
|
9
|
+
DEGRADED: 'degraded',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Ping an endpoint and return status
|
|
14
|
+
* @param {string} url - URL to ping
|
|
15
|
+
* @param {Object} options - Configuration options
|
|
16
|
+
* @param {Function} options.fetch - Fetch function
|
|
17
|
+
* @param {number} options.timeout - Timeout in ms
|
|
18
|
+
* @returns {Object} Ping result with status and response time
|
|
19
|
+
*/
|
|
20
|
+
export async function pingEndpoint(url, options = {}) {
|
|
21
|
+
const { fetch: fetchFn = fetch, timeout = 5000 } = options;
|
|
22
|
+
|
|
23
|
+
const startTime = Date.now();
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
28
|
+
|
|
29
|
+
const fetchPromise = fetchFn(url, { signal: controller.signal });
|
|
30
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
31
|
+
setTimeout(() => reject(new Error('timeout')), timeout)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const response = await Promise.race([fetchPromise, timeoutPromise]);
|
|
35
|
+
clearTimeout(timeoutId);
|
|
36
|
+
|
|
37
|
+
const responseTime = Date.now() - startTime;
|
|
38
|
+
|
|
39
|
+
if (response.ok) {
|
|
40
|
+
return {
|
|
41
|
+
status: UPTIME_STATUS.UP,
|
|
42
|
+
responseTime,
|
|
43
|
+
statusCode: response.status,
|
|
44
|
+
};
|
|
45
|
+
} else {
|
|
46
|
+
return {
|
|
47
|
+
status: UPTIME_STATUS.DOWN,
|
|
48
|
+
responseTime,
|
|
49
|
+
statusCode: response.status,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const responseTime = Date.now() - startTime;
|
|
54
|
+
|
|
55
|
+
if (error.message === 'timeout' || error.name === 'AbortError') {
|
|
56
|
+
return {
|
|
57
|
+
status: UPTIME_STATUS.DOWN,
|
|
58
|
+
responseTime,
|
|
59
|
+
error: 'timeout',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
status: UPTIME_STATUS.DOWN,
|
|
65
|
+
responseTime,
|
|
66
|
+
error: error.message,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Calculate uptime percentage from check results
|
|
73
|
+
* @param {Array} checks - Array of check results
|
|
74
|
+
* @returns {number} Uptime percentage
|
|
75
|
+
*/
|
|
76
|
+
export function calculateUptime(checks) {
|
|
77
|
+
if (checks.length === 0) return 100;
|
|
78
|
+
|
|
79
|
+
const upCount = checks.filter((check) => check.status === UPTIME_STATUS.UP).length;
|
|
80
|
+
return (upCount / checks.length) * 100;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Generate an uptime report
|
|
85
|
+
* @param {Object} options - Report options
|
|
86
|
+
* @param {string} options.endpoint - Endpoint URL
|
|
87
|
+
* @param {string} options.period - Report period (day, week, month)
|
|
88
|
+
* @param {Array} options.checks - Check results
|
|
89
|
+
* @returns {Object} Uptime report
|
|
90
|
+
*/
|
|
91
|
+
export function generateUptimeReport(options = {}) {
|
|
92
|
+
const { endpoint, period = 'day', checks = [] } = options;
|
|
93
|
+
|
|
94
|
+
const uptime = calculateUptime(checks);
|
|
95
|
+
|
|
96
|
+
// Calculate average response time
|
|
97
|
+
const responseTimes = checks
|
|
98
|
+
.filter((c) => c.responseTime !== undefined)
|
|
99
|
+
.map((c) => c.responseTime);
|
|
100
|
+
|
|
101
|
+
const avgResponseTime =
|
|
102
|
+
responseTimes.length > 0
|
|
103
|
+
? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length
|
|
104
|
+
: 0;
|
|
105
|
+
|
|
106
|
+
// Count incidents (transitions from up to down)
|
|
107
|
+
let incidents = 0;
|
|
108
|
+
for (let i = 1; i < checks.length; i++) {
|
|
109
|
+
if (checks[i - 1].status === UPTIME_STATUS.UP && checks[i].status === UPTIME_STATUS.DOWN) {
|
|
110
|
+
incidents++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
endpoint,
|
|
116
|
+
period,
|
|
117
|
+
uptime,
|
|
118
|
+
avgResponseTime,
|
|
119
|
+
totalChecks: checks.length,
|
|
120
|
+
incidents,
|
|
121
|
+
generatedAt: new Date().toISOString(),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Create an uptime monitor
|
|
127
|
+
* @param {Object} options - Configuration options
|
|
128
|
+
* @param {Function} options.fetch - Fetch function
|
|
129
|
+
* @param {number} options.interval - Check interval in ms
|
|
130
|
+
* @returns {Object} Uptime monitor
|
|
131
|
+
*/
|
|
132
|
+
export function createUptimeMonitor(options = {}) {
|
|
133
|
+
const { fetch: fetchFn = fetch, interval = 60000 } = options;
|
|
134
|
+
|
|
135
|
+
const endpoints = new Map();
|
|
136
|
+
const checkHistory = new Map();
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
addEndpoint(url, endpointOptions = {}) {
|
|
140
|
+
endpoints.set(url, {
|
|
141
|
+
url,
|
|
142
|
+
...endpointOptions,
|
|
143
|
+
});
|
|
144
|
+
checkHistory.set(url, []);
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
removeEndpoint(url) {
|
|
148
|
+
endpoints.delete(url);
|
|
149
|
+
checkHistory.delete(url);
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
getEndpoints() {
|
|
153
|
+
return Array.from(endpoints.values());
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
async check(url) {
|
|
157
|
+
const urlsToCheck = url ? [url] : Array.from(endpoints.keys());
|
|
158
|
+
|
|
159
|
+
for (const endpointUrl of urlsToCheck) {
|
|
160
|
+
const result = await pingEndpoint(endpointUrl, { fetch: fetchFn });
|
|
161
|
+
result.timestamp = Date.now();
|
|
162
|
+
|
|
163
|
+
const history = checkHistory.get(endpointUrl) || [];
|
|
164
|
+
history.push(result);
|
|
165
|
+
checkHistory.set(endpointUrl, history);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
getStatus(url) {
|
|
170
|
+
const history = checkHistory.get(url);
|
|
171
|
+
if (!history || history.length === 0) {
|
|
172
|
+
return UPTIME_STATUS.UP; // Default to up if no checks
|
|
173
|
+
}
|
|
174
|
+
return history[history.length - 1].status;
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
getHistory(url) {
|
|
178
|
+
return checkHistory.get(url) || [];
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
getReport(url, period = 'day') {
|
|
182
|
+
const history = checkHistory.get(url) || [];
|
|
183
|
+
return generateUptimeReport({
|
|
184
|
+
endpoint: url,
|
|
185
|
+
period,
|
|
186
|
+
checks: history,
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
getInterval() {
|
|
191
|
+
return interval;
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|