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.
Files changed (169) hide show
  1. package/package.json +1 -1
  2. package/server/index.js +229 -14
  3. package/server/lib/compliance/control-mapper.js +401 -0
  4. package/server/lib/compliance/control-mapper.test.js +117 -0
  5. package/server/lib/compliance/evidence-linker.js +296 -0
  6. package/server/lib/compliance/evidence-linker.test.js +121 -0
  7. package/server/lib/compliance/gdpr-checklist.js +416 -0
  8. package/server/lib/compliance/gdpr-checklist.test.js +131 -0
  9. package/server/lib/compliance/hipaa-checklist.js +277 -0
  10. package/server/lib/compliance/hipaa-checklist.test.js +101 -0
  11. package/server/lib/compliance/iso27001-checklist.js +287 -0
  12. package/server/lib/compliance/iso27001-checklist.test.js +99 -0
  13. package/server/lib/compliance/multi-framework-reporter.js +284 -0
  14. package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
  15. package/server/lib/compliance/pci-dss-checklist.js +214 -0
  16. package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
  17. package/server/lib/compliance/trust-centre.js +187 -0
  18. package/server/lib/compliance/trust-centre.test.js +93 -0
  19. package/server/lib/dashboard/api-server.js +155 -0
  20. package/server/lib/dashboard/api-server.test.js +155 -0
  21. package/server/lib/dashboard/health-api.js +199 -0
  22. package/server/lib/dashboard/health-api.test.js +122 -0
  23. package/server/lib/dashboard/notes-api.js +234 -0
  24. package/server/lib/dashboard/notes-api.test.js +134 -0
  25. package/server/lib/dashboard/router-api.js +176 -0
  26. package/server/lib/dashboard/router-api.test.js +132 -0
  27. package/server/lib/dashboard/tasks-api.js +289 -0
  28. package/server/lib/dashboard/tasks-api.test.js +161 -0
  29. package/server/lib/dashboard/tlc-introspection.js +197 -0
  30. package/server/lib/dashboard/tlc-introspection.test.js +138 -0
  31. package/server/lib/dashboard/version-api.js +222 -0
  32. package/server/lib/dashboard/version-api.test.js +112 -0
  33. package/server/lib/dashboard/websocket-server.js +104 -0
  34. package/server/lib/dashboard/websocket-server.test.js +118 -0
  35. package/server/lib/deploy/branch-classifier.js +163 -0
  36. package/server/lib/deploy/branch-classifier.test.js +164 -0
  37. package/server/lib/deploy/deployment-approval.js +299 -0
  38. package/server/lib/deploy/deployment-approval.test.js +296 -0
  39. package/server/lib/deploy/deployment-audit.js +374 -0
  40. package/server/lib/deploy/deployment-audit.test.js +307 -0
  41. package/server/lib/deploy/deployment-executor.js +335 -0
  42. package/server/lib/deploy/deployment-executor.test.js +329 -0
  43. package/server/lib/deploy/deployment-rules.js +163 -0
  44. package/server/lib/deploy/deployment-rules.test.js +188 -0
  45. package/server/lib/deploy/rollback-manager.js +379 -0
  46. package/server/lib/deploy/rollback-manager.test.js +321 -0
  47. package/server/lib/deploy/security-gates.js +236 -0
  48. package/server/lib/deploy/security-gates.test.js +222 -0
  49. package/server/lib/k8s/gitops-config.js +188 -0
  50. package/server/lib/k8s/gitops-config.test.js +59 -0
  51. package/server/lib/k8s/helm-generator.js +196 -0
  52. package/server/lib/k8s/helm-generator.test.js +59 -0
  53. package/server/lib/k8s/kustomize-generator.js +176 -0
  54. package/server/lib/k8s/kustomize-generator.test.js +58 -0
  55. package/server/lib/k8s/network-policy.js +114 -0
  56. package/server/lib/k8s/network-policy.test.js +53 -0
  57. package/server/lib/k8s/pod-security.js +114 -0
  58. package/server/lib/k8s/pod-security.test.js +55 -0
  59. package/server/lib/k8s/rbac-generator.js +132 -0
  60. package/server/lib/k8s/rbac-generator.test.js +57 -0
  61. package/server/lib/k8s/resource-manager.js +172 -0
  62. package/server/lib/k8s/resource-manager.test.js +60 -0
  63. package/server/lib/k8s/secrets-encryption.js +168 -0
  64. package/server/lib/k8s/secrets-encryption.test.js +49 -0
  65. package/server/lib/monitoring/alert-manager.js +238 -0
  66. package/server/lib/monitoring/alert-manager.test.js +106 -0
  67. package/server/lib/monitoring/health-check.js +226 -0
  68. package/server/lib/monitoring/health-check.test.js +176 -0
  69. package/server/lib/monitoring/incident-manager.js +230 -0
  70. package/server/lib/monitoring/incident-manager.test.js +98 -0
  71. package/server/lib/monitoring/log-aggregator.js +147 -0
  72. package/server/lib/monitoring/log-aggregator.test.js +89 -0
  73. package/server/lib/monitoring/metrics-collector.js +337 -0
  74. package/server/lib/monitoring/metrics-collector.test.js +172 -0
  75. package/server/lib/monitoring/status-page.js +214 -0
  76. package/server/lib/monitoring/status-page.test.js +105 -0
  77. package/server/lib/monitoring/uptime-monitor.js +194 -0
  78. package/server/lib/monitoring/uptime-monitor.test.js +109 -0
  79. package/server/lib/network/fail2ban-config.js +294 -0
  80. package/server/lib/network/fail2ban-config.test.js +275 -0
  81. package/server/lib/network/firewall-manager.js +252 -0
  82. package/server/lib/network/firewall-manager.test.js +254 -0
  83. package/server/lib/network/geoip-filter.js +282 -0
  84. package/server/lib/network/geoip-filter.test.js +264 -0
  85. package/server/lib/network/rate-limiter.js +229 -0
  86. package/server/lib/network/rate-limiter.test.js +293 -0
  87. package/server/lib/network/request-validator.js +351 -0
  88. package/server/lib/network/request-validator.test.js +345 -0
  89. package/server/lib/network/security-headers.js +251 -0
  90. package/server/lib/network/security-headers.test.js +283 -0
  91. package/server/lib/network/tls-config.js +210 -0
  92. package/server/lib/network/tls-config.test.js +248 -0
  93. package/server/lib/security/auth-security.js +369 -0
  94. package/server/lib/security/auth-security.test.js +448 -0
  95. package/server/lib/security/cis-benchmark.js +152 -0
  96. package/server/lib/security/cis-benchmark.test.js +137 -0
  97. package/server/lib/security/compose-templates.js +312 -0
  98. package/server/lib/security/compose-templates.test.js +229 -0
  99. package/server/lib/security/container-runtime.js +456 -0
  100. package/server/lib/security/container-runtime.test.js +503 -0
  101. package/server/lib/security/cors-validator.js +278 -0
  102. package/server/lib/security/cors-validator.test.js +310 -0
  103. package/server/lib/security/crypto-utils.js +253 -0
  104. package/server/lib/security/crypto-utils.test.js +409 -0
  105. package/server/lib/security/dockerfile-linter.js +459 -0
  106. package/server/lib/security/dockerfile-linter.test.js +483 -0
  107. package/server/lib/security/dockerfile-templates.js +278 -0
  108. package/server/lib/security/dockerfile-templates.test.js +164 -0
  109. package/server/lib/security/error-sanitizer.js +426 -0
  110. package/server/lib/security/error-sanitizer.test.js +331 -0
  111. package/server/lib/security/headers-generator.js +368 -0
  112. package/server/lib/security/headers-generator.test.js +398 -0
  113. package/server/lib/security/image-scanner.js +83 -0
  114. package/server/lib/security/image-scanner.test.js +106 -0
  115. package/server/lib/security/input-validator.js +352 -0
  116. package/server/lib/security/input-validator.test.js +330 -0
  117. package/server/lib/security/network-policy.js +174 -0
  118. package/server/lib/security/network-policy.test.js +164 -0
  119. package/server/lib/security/output-encoder.js +237 -0
  120. package/server/lib/security/output-encoder.test.js +276 -0
  121. package/server/lib/security/path-validator.js +359 -0
  122. package/server/lib/security/path-validator.test.js +293 -0
  123. package/server/lib/security/query-builder.js +421 -0
  124. package/server/lib/security/query-builder.test.js +318 -0
  125. package/server/lib/security/secret-detector.js +290 -0
  126. package/server/lib/security/secret-detector.test.js +354 -0
  127. package/server/lib/security/secrets-validator.js +137 -0
  128. package/server/lib/security/secrets-validator.test.js +120 -0
  129. package/server/lib/security-testing/dast-runner.js +154 -0
  130. package/server/lib/security-testing/dast-runner.test.js +62 -0
  131. package/server/lib/security-testing/dependency-scanner.js +172 -0
  132. package/server/lib/security-testing/dependency-scanner.test.js +64 -0
  133. package/server/lib/security-testing/pentest-runner.js +230 -0
  134. package/server/lib/security-testing/pentest-runner.test.js +60 -0
  135. package/server/lib/security-testing/sast-runner.js +136 -0
  136. package/server/lib/security-testing/sast-runner.test.js +62 -0
  137. package/server/lib/security-testing/secret-scanner.js +153 -0
  138. package/server/lib/security-testing/secret-scanner.test.js +66 -0
  139. package/server/lib/security-testing/security-gate.js +216 -0
  140. package/server/lib/security-testing/security-gate.test.js +115 -0
  141. package/server/lib/security-testing/security-reporter.js +303 -0
  142. package/server/lib/security-testing/security-reporter.test.js +114 -0
  143. package/server/lib/standards/audit-checker.js +546 -0
  144. package/server/lib/standards/audit-checker.test.js +415 -0
  145. package/server/lib/standards/cleanup-executor.js +452 -0
  146. package/server/lib/standards/cleanup-executor.test.js +293 -0
  147. package/server/lib/standards/refactor-stepper.js +425 -0
  148. package/server/lib/standards/refactor-stepper.test.js +298 -0
  149. package/server/lib/standards/standards-injector.js +167 -0
  150. package/server/lib/standards/standards-injector.test.js +232 -0
  151. package/server/lib/user-management.test.js +284 -0
  152. package/server/lib/vps/backup-manager.js +157 -0
  153. package/server/lib/vps/backup-manager.test.js +59 -0
  154. package/server/lib/vps/caddy-config.js +159 -0
  155. package/server/lib/vps/caddy-config.test.js +48 -0
  156. package/server/lib/vps/compose-orchestrator.js +219 -0
  157. package/server/lib/vps/compose-orchestrator.test.js +50 -0
  158. package/server/lib/vps/database-config.js +208 -0
  159. package/server/lib/vps/database-config.test.js +47 -0
  160. package/server/lib/vps/deploy-script.js +211 -0
  161. package/server/lib/vps/deploy-script.test.js +53 -0
  162. package/server/lib/vps/secrets-manager.js +148 -0
  163. package/server/lib/vps/secrets-manager.test.js +58 -0
  164. package/server/lib/vps/server-hardening.js +174 -0
  165. package/server/lib/vps/server-hardening.test.js +70 -0
  166. package/server/package-lock.json +19 -0
  167. package/server/package.json +1 -0
  168. package/server/templates/CLAUDE.md +37 -0
  169. 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
+ }