@vizzly-testing/cli 0.8.0 → 0.9.0

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 (70) hide show
  1. package/README.md +26 -13
  2. package/dist/cli.js +25 -1
  3. package/dist/client/index.js +77 -11
  4. package/dist/commands/init.js +23 -17
  5. package/dist/commands/tdd-daemon.js +312 -0
  6. package/dist/commands/tdd.js +45 -14
  7. package/dist/reporter/reporter-bundle.css +1 -0
  8. package/dist/reporter/reporter-bundle.iife.js +57 -0
  9. package/dist/server/handlers/api-handler.js +98 -30
  10. package/dist/server/handlers/tdd-handler.js +264 -77
  11. package/dist/server/http-server.js +358 -15
  12. package/dist/services/html-report-generator.js +77 -0
  13. package/dist/services/report-generator/report.css +56 -0
  14. package/dist/services/screenshot-server.js +6 -3
  15. package/dist/services/server-manager.js +2 -9
  16. package/dist/services/tdd-service.js +188 -25
  17. package/dist/services/test-runner.js +43 -1
  18. package/dist/types/commands/tdd-daemon.d.ts +18 -0
  19. package/dist/types/container/index.d.ts +1 -3
  20. package/dist/types/reporter/src/components/app-router.d.ts +3 -0
  21. package/dist/types/reporter/src/components/comparison/comparison-actions.d.ts +5 -0
  22. package/dist/types/reporter/src/components/comparison/comparison-card.d.ts +6 -0
  23. package/dist/types/reporter/src/components/comparison/comparison-list.d.ts +6 -0
  24. package/dist/types/reporter/src/components/comparison/comparison-viewer.d.ts +4 -0
  25. package/dist/types/reporter/src/components/comparison/view-mode-selector.d.ts +4 -0
  26. package/dist/types/reporter/src/components/comparison/viewer-modes/onion-viewer.d.ts +3 -0
  27. package/dist/types/reporter/src/components/comparison/viewer-modes/overlay-viewer.d.ts +3 -0
  28. package/dist/types/reporter/src/components/comparison/viewer-modes/side-by-side-viewer.d.ts +3 -0
  29. package/dist/types/reporter/src/components/comparison/viewer-modes/toggle-viewer.d.ts +3 -0
  30. package/dist/types/reporter/src/components/dashboard/dashboard-filters.d.ts +16 -0
  31. package/dist/types/reporter/src/components/dashboard/dashboard-header.d.ts +5 -0
  32. package/dist/types/reporter/src/components/dashboard/dashboard-stats.d.ts +4 -0
  33. package/dist/types/reporter/src/components/dashboard/empty-state.d.ts +8 -0
  34. package/dist/types/reporter/src/components/ui/smart-image.d.ts +7 -0
  35. package/dist/types/reporter/src/components/ui/status-badge.d.ts +5 -0
  36. package/dist/types/reporter/src/components/ui/toast.d.ts +4 -0
  37. package/dist/types/reporter/src/components/views/comparisons-view.d.ts +6 -0
  38. package/dist/types/reporter/src/components/views/stats-view.d.ts +6 -0
  39. package/dist/types/reporter/src/hooks/use-baseline-actions.d.ts +5 -0
  40. package/dist/types/reporter/src/hooks/use-comparison-filters.d.ts +20 -0
  41. package/dist/types/reporter/src/hooks/use-image-loader.d.ts +1 -0
  42. package/dist/types/reporter/src/hooks/use-report-data.d.ts +7 -0
  43. package/dist/types/reporter/src/hooks/use-vizzly-api.d.ts +9 -0
  44. package/dist/types/reporter/src/main.d.ts +1 -0
  45. package/dist/types/reporter/src/services/api-client.d.ts +4 -0
  46. package/dist/types/reporter/src/utils/comparison-helpers.d.ts +16 -0
  47. package/dist/types/reporter/src/utils/constants.d.ts +37 -0
  48. package/dist/types/reporter/vite.config.d.ts +2 -0
  49. package/dist/types/reporter/vite.dev.config.d.ts +2 -0
  50. package/dist/types/sdk/index.d.ts +1 -2
  51. package/dist/types/server/handlers/api-handler.d.ts +5 -14
  52. package/dist/types/server/handlers/tdd-handler.d.ts +18 -17
  53. package/dist/types/server/http-server.d.ts +2 -1
  54. package/dist/types/services/base-service.d.ts +1 -2
  55. package/dist/types/services/html-report-generator.d.ts +3 -3
  56. package/dist/types/services/screenshot-server.d.ts +1 -1
  57. package/dist/types/services/server-manager.d.ts +25 -35
  58. package/dist/types/services/tdd-service.d.ts +7 -1
  59. package/dist/types/services/test-runner.d.ts +6 -1
  60. package/dist/types/utils/build-history.d.ts +16 -0
  61. package/dist/types/utils/config-loader.d.ts +1 -1
  62. package/dist/types/utils/console-ui.d.ts +1 -1
  63. package/dist/types/utils/git.d.ts +4 -4
  64. package/dist/types/utils/security.d.ts +2 -1
  65. package/dist/utils/build-history.js +103 -0
  66. package/dist/utils/security.js +14 -5
  67. package/docs/api-reference.md +1 -3
  68. package/docs/getting-started.md +1 -1
  69. package/docs/tdd-mode.md +176 -112
  70. package/package.json +17 -4
@@ -1,7 +1,13 @@
1
1
  import { createServer } from 'http';
2
+ import { readFileSync, existsSync } from 'fs';
3
+ import { join, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
2
5
  import { createServiceLogger } from '../utils/logger-factory.js';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ const PROJECT_ROOT = join(__dirname, '..', '..');
3
9
  const logger = createServiceLogger('HTTP-SERVER');
4
- export const createHttpServer = (port, screenshotHandler, emitter = null) => {
10
+ export const createHttpServer = (port, screenshotHandler) => {
5
11
  let server = null;
6
12
  const parseRequestBody = req => {
7
13
  return new Promise((resolve, reject) => {
@@ -30,16 +36,304 @@ export const createHttpServer = (port, screenshotHandler, emitter = null) => {
30
36
  res.end();
31
37
  return;
32
38
  }
33
- if (req.method === 'GET' && req.url === '/health') {
39
+
40
+ // Parse URL to handle query params properly for all routes
41
+ const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
42
+ if (req.method === 'GET' && parsedUrl.pathname === '/health') {
43
+ // Enhanced health endpoint with diagnostics
44
+ const reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
45
+ const baselineMetadataPath = join(process.cwd(), '.vizzly', 'baselines', 'metadata.json');
46
+ let reportData = null;
47
+ let baselineInfo = null;
48
+ if (existsSync(reportDataPath)) {
49
+ try {
50
+ reportData = JSON.parse(readFileSync(reportDataPath, 'utf8'));
51
+ } catch {
52
+ // Ignore read errors
53
+ }
54
+ }
55
+ if (existsSync(baselineMetadataPath)) {
56
+ try {
57
+ baselineInfo = JSON.parse(readFileSync(baselineMetadataPath, 'utf8'));
58
+ } catch {
59
+ // Ignore read errors
60
+ }
61
+ }
34
62
  res.statusCode = 200;
35
63
  res.end(JSON.stringify({
36
64
  status: 'ok',
37
65
  port: port,
38
- uptime: process.uptime()
66
+ uptime: process.uptime(),
67
+ mode: screenshotHandler ? 'tdd' : 'upload',
68
+ baseline: baselineInfo ? {
69
+ buildName: baselineInfo.buildName,
70
+ createdAt: baselineInfo.createdAt
71
+ } : null,
72
+ stats: reportData ? {
73
+ total: reportData.summary?.total || 0,
74
+ passed: reportData.summary?.passed || 0,
75
+ failed: reportData.summary?.failed || 0,
76
+ errors: reportData.summary?.errors || 0
77
+ } : null
39
78
  }));
40
79
  return;
41
80
  }
42
- if (req.method === 'POST' && req.url === '/screenshot') {
81
+
82
+ // Serve the main React app for all non-API routes
83
+ if (req.method === 'GET' && (parsedUrl.pathname === '/' || parsedUrl.pathname === '/dashboard' || parsedUrl.pathname === '/stats')) {
84
+ // Serve React-powered dashboard
85
+ const reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
86
+
87
+ // Try to read existing report data
88
+ let reportData = null;
89
+ if (existsSync(reportDataPath)) {
90
+ try {
91
+ const data = readFileSync(reportDataPath, 'utf8');
92
+ reportData = JSON.parse(data);
93
+ } catch (error) {
94
+ logger.debug('Could not read report data:', error.message);
95
+ }
96
+ }
97
+ const dashboardHtml = `
98
+ <!DOCTYPE html>
99
+ <html>
100
+ <head>
101
+ <title>Vizzly TDD Dashboard</title>
102
+ <meta charset="utf-8">
103
+ <meta name="viewport" content="width=device-width, initial-scale=1">
104
+ <link rel="stylesheet" href="/reporter-bundle.css">
105
+ </head>
106
+ <body>
107
+ <div id="vizzly-reporter-root">
108
+ <div class="reporter-loading">
109
+ <div>
110
+ <div class="spinner"></div>
111
+ <p>Loading Vizzly TDD Dashboard...</p>
112
+ </div>
113
+ </div>
114
+ </div>
115
+
116
+ <script>
117
+ // Inject report data if available
118
+ ${reportData ? `window.VIZZLY_REPORTER_DATA = ${JSON.stringify(reportData)};` : ''}
119
+ </script>
120
+ <script src="/reporter-bundle.js"></script>
121
+ </body>
122
+ </html>`;
123
+ res.setHeader('Content-Type', 'text/html');
124
+ res.statusCode = 200;
125
+ res.end(dashboardHtml);
126
+ return;
127
+ }
128
+ if (req.method === 'GET' && parsedUrl.pathname === '/reporter-bundle.js') {
129
+ // Serve the React bundle
130
+ const bundlePath = join(PROJECT_ROOT, 'dist', 'reporter', 'reporter-bundle.iife.js');
131
+ if (existsSync(bundlePath)) {
132
+ try {
133
+ const bundle = readFileSync(bundlePath, 'utf8');
134
+ res.setHeader('Content-Type', 'application/javascript');
135
+ res.statusCode = 200;
136
+ res.end(bundle);
137
+ } catch (error) {
138
+ logger.error('Error serving reporter bundle:', error);
139
+ res.statusCode = 500;
140
+ res.end('Error loading reporter bundle');
141
+ }
142
+ } else {
143
+ res.statusCode = 404;
144
+ res.end('Reporter bundle not found');
145
+ }
146
+ return;
147
+ }
148
+ if (req.method === 'GET' && parsedUrl.pathname === '/reporter-bundle.css') {
149
+ // Serve the React CSS bundle
150
+ const cssPath = join(PROJECT_ROOT, 'dist', 'reporter', 'reporter-bundle.css');
151
+ if (existsSync(cssPath)) {
152
+ try {
153
+ const css = readFileSync(cssPath, 'utf8');
154
+ res.setHeader('Content-Type', 'text/css');
155
+ res.statusCode = 200;
156
+ res.end(css);
157
+ } catch (error) {
158
+ logger.error('Error serving reporter CSS:', error);
159
+ res.statusCode = 500;
160
+ res.end('Error loading reporter CSS');
161
+ }
162
+ } else {
163
+ res.statusCode = 404;
164
+ res.end('Reporter CSS not found');
165
+ }
166
+ return;
167
+ }
168
+ if (req.method === 'GET' && parsedUrl.pathname === '/api/report-data') {
169
+ // API endpoint for fetching report data
170
+ const reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
171
+ if (existsSync(reportDataPath)) {
172
+ try {
173
+ const data = readFileSync(reportDataPath, 'utf8');
174
+ res.setHeader('Content-Type', 'application/json');
175
+ res.statusCode = 200;
176
+ res.end(data);
177
+ } catch (error) {
178
+ logger.error('Error reading report data:', error);
179
+ res.statusCode = 500;
180
+ res.end(JSON.stringify({
181
+ error: 'Failed to read report data'
182
+ }));
183
+ }
184
+ } else {
185
+ res.statusCode = 200;
186
+ res.end(JSON.stringify(null)); // No data available yet
187
+ }
188
+ return;
189
+ }
190
+ if (req.method === 'GET' && parsedUrl.pathname === '/api/status') {
191
+ // Real-time status endpoint
192
+ const reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
193
+ const baselineMetadataPath = join(process.cwd(), '.vizzly', 'baselines', 'metadata.json');
194
+ let reportData = null;
195
+ let baselineInfo = null;
196
+ if (existsSync(reportDataPath)) {
197
+ try {
198
+ reportData = JSON.parse(readFileSync(reportDataPath, 'utf8'));
199
+ } catch {
200
+ // Ignore
201
+ }
202
+ }
203
+ if (existsSync(baselineMetadataPath)) {
204
+ try {
205
+ baselineInfo = JSON.parse(readFileSync(baselineMetadataPath, 'utf8'));
206
+ } catch {
207
+ // Ignore
208
+ }
209
+ }
210
+ res.setHeader('Content-Type', 'application/json');
211
+ res.statusCode = 200;
212
+ res.end(JSON.stringify({
213
+ timestamp: Date.now(),
214
+ baseline: baselineInfo,
215
+ comparisons: reportData?.comparisons || [],
216
+ summary: reportData?.summary || {
217
+ total: 0,
218
+ passed: 0,
219
+ failed: 0,
220
+ errors: 0
221
+ }
222
+ }));
223
+ return;
224
+ }
225
+ if (req.method === 'POST' && parsedUrl.pathname === '/api/baseline/accept') {
226
+ // Accept a single screenshot as baseline
227
+ if (!screenshotHandler?.acceptBaseline) {
228
+ res.statusCode = 400;
229
+ res.end(JSON.stringify({
230
+ error: 'Baseline management not available'
231
+ }));
232
+ return;
233
+ }
234
+ try {
235
+ const {
236
+ name
237
+ } = await parseRequestBody(req);
238
+ if (!name) {
239
+ res.statusCode = 400;
240
+ res.end(JSON.stringify({
241
+ error: 'Screenshot name required'
242
+ }));
243
+ return;
244
+ }
245
+ await screenshotHandler.acceptBaseline(name);
246
+ res.setHeader('Content-Type', 'application/json');
247
+ res.statusCode = 200;
248
+ res.end(JSON.stringify({
249
+ success: true,
250
+ message: `Baseline accepted for ${name}`
251
+ }));
252
+ } catch (error) {
253
+ logger.error('Error accepting baseline:', error);
254
+ res.statusCode = 500;
255
+ res.end(JSON.stringify({
256
+ error: error.message
257
+ }));
258
+ }
259
+ return;
260
+ }
261
+ if (req.method === 'POST' && parsedUrl.pathname === '/api/baseline/accept-all') {
262
+ // Accept all screenshots as baseline
263
+ if (!screenshotHandler?.acceptAllBaselines) {
264
+ res.statusCode = 400;
265
+ res.end(JSON.stringify({
266
+ error: 'Baseline management not available'
267
+ }));
268
+ return;
269
+ }
270
+ try {
271
+ const result = await screenshotHandler.acceptAllBaselines();
272
+ res.setHeader('Content-Type', 'application/json');
273
+ res.statusCode = 200;
274
+ res.end(JSON.stringify({
275
+ success: true,
276
+ message: `Accepted ${result.count} baselines`,
277
+ count: result.count
278
+ }));
279
+ } catch (error) {
280
+ logger.error('Error accepting all baselines:', error);
281
+ res.statusCode = 500;
282
+ res.end(JSON.stringify({
283
+ error: error.message
284
+ }));
285
+ }
286
+ return;
287
+ }
288
+ if (req.method === 'POST' && parsedUrl.pathname === '/api/baseline/reset') {
289
+ // Reset baselines to previous state
290
+ if (!screenshotHandler?.resetBaselines) {
291
+ res.statusCode = 400;
292
+ res.end(JSON.stringify({
293
+ error: 'Baseline management not available'
294
+ }));
295
+ return;
296
+ }
297
+ try {
298
+ await screenshotHandler.resetBaselines();
299
+ res.setHeader('Content-Type', 'application/json');
300
+ res.statusCode = 200;
301
+ res.end(JSON.stringify({
302
+ success: true,
303
+ message: 'Baselines reset to previous state'
304
+ }));
305
+ } catch (error) {
306
+ logger.error('Error resetting baselines:', error);
307
+ res.statusCode = 500;
308
+ res.end(JSON.stringify({
309
+ error: error.message
310
+ }));
311
+ }
312
+ return;
313
+ }
314
+
315
+ // Serve images from .vizzly directory
316
+ if (req.method === 'GET' && parsedUrl.pathname.startsWith('/images/')) {
317
+ const imagePath = parsedUrl.pathname.replace('/images/', '');
318
+ const fullImagePath = join(process.cwd(), '.vizzly', imagePath);
319
+ if (existsSync(fullImagePath)) {
320
+ try {
321
+ const imageData = readFileSync(fullImagePath);
322
+ res.setHeader('Content-Type', 'image/png');
323
+ res.statusCode = 200;
324
+ res.end(imageData);
325
+ } catch (error) {
326
+ logger.error('Error serving image:', error);
327
+ res.statusCode = 500;
328
+ res.end('Error loading image');
329
+ }
330
+ } else {
331
+ res.statusCode = 404;
332
+ res.end('Image not found');
333
+ }
334
+ return;
335
+ }
336
+ if (req.method === 'POST' && parsedUrl.pathname === '/screenshot') {
43
337
  try {
44
338
  const body = await parseRequestBody(req);
45
339
  const {
@@ -48,23 +342,17 @@ export const createHttpServer = (port, screenshotHandler, emitter = null) => {
48
342
  properties,
49
343
  image
50
344
  } = body;
51
- if (!buildId || !name || !image) {
345
+ if (!name || !image) {
52
346
  res.statusCode = 400;
53
347
  res.end(JSON.stringify({
54
- error: 'buildId, name, and image are required'
348
+ error: 'name and image are required'
55
349
  }));
56
350
  return;
57
351
  }
58
- const result = await screenshotHandler.handleScreenshot(buildId, name, image, properties);
59
352
 
60
- // Emit screenshot captured event if emitter is available
61
- if (emitter && result.statusCode === 200) {
62
- emitter.emit('screenshot-captured', {
63
- name,
64
- count: screenshotHandler.getScreenshotCount?.(buildId) || 0,
65
- skipped: result.body?.skipped
66
- });
67
- }
353
+ // Use default buildId if none provided
354
+ const effectiveBuildId = buildId || 'default';
355
+ const result = await screenshotHandler.handleScreenshot(effectiveBuildId, name, image, properties);
68
356
  res.statusCode = result.statusCode;
69
357
  res.end(JSON.stringify(result.body));
70
358
  } catch (error) {
@@ -76,6 +364,43 @@ export const createHttpServer = (port, screenshotHandler, emitter = null) => {
76
364
  }
77
365
  return;
78
366
  }
367
+ if (req.method === 'POST' && parsedUrl.pathname === '/accept-baseline') {
368
+ try {
369
+ const body = await parseRequestBody(req);
370
+ const {
371
+ name
372
+ } = body;
373
+ if (!name) {
374
+ res.statusCode = 400;
375
+ res.end(JSON.stringify({
376
+ error: 'screenshot name is required'
377
+ }));
378
+ return;
379
+ }
380
+
381
+ // Call the screenshot handler's accept baseline method if it exists
382
+ if (screenshotHandler.acceptBaseline) {
383
+ const result = await screenshotHandler.acceptBaseline(name);
384
+ res.statusCode = 200;
385
+ res.end(JSON.stringify({
386
+ success: true,
387
+ ...result
388
+ }));
389
+ } else {
390
+ res.statusCode = 501;
391
+ res.end(JSON.stringify({
392
+ error: 'Accept baseline not implemented'
393
+ }));
394
+ }
395
+ } catch (error) {
396
+ logger.error('Accept baseline error:', error);
397
+ res.statusCode = 500;
398
+ res.end(JSON.stringify({
399
+ error: 'Failed to accept baseline'
400
+ }));
401
+ }
402
+ return;
403
+ }
79
404
  res.statusCode = 404;
80
405
  res.end(JSON.stringify({
81
406
  error: 'Not found'
@@ -124,9 +449,27 @@ export const createHttpServer = (port, screenshotHandler, emitter = null) => {
124
449
  }
125
450
  return Promise.resolve();
126
451
  };
452
+
453
+ /**
454
+ * Finish build - flush any pending background operations
455
+ * Call this before finalizing a build to ensure all uploads complete
456
+ */
457
+ const finishBuild = async buildId => {
458
+ logger.debug(`Finishing build ${buildId}...`);
459
+
460
+ // Flush screenshot handler if it has a flush method (API mode)
461
+ if (screenshotHandler?.flush) {
462
+ const stats = await screenshotHandler.flush();
463
+ logger.debug(`Build ${buildId} uploads complete: ${stats.uploaded} uploaded, ${stats.failed} failed`);
464
+ return stats;
465
+ }
466
+ logger.debug(`Build ${buildId} finished (no flush needed)`);
467
+ return null;
468
+ };
127
469
  return {
128
470
  start,
129
471
  stop,
472
+ finishBuild,
130
473
  getServer: () => server
131
474
  };
132
475
  };
@@ -297,6 +297,74 @@ document.addEventListener('DOMContentLoaded', function () {
297
297
 
298
298
  console.log('Vizzly TDD Report loaded successfully');
299
299
  });
300
+
301
+ // Accept/Reject baseline functions
302
+ async function acceptBaseline(screenshotName) {
303
+ const button = document.querySelector(\`button[onclick*="\${screenshotName}"]\`);
304
+ if (button) {
305
+ button.disabled = true;
306
+ button.innerHTML = '⏳ Accepting...';
307
+ }
308
+
309
+ try {
310
+ const response = await fetch('/accept-baseline', {
311
+ method: 'POST',
312
+ headers: { 'Content-Type': 'application/json' },
313
+ body: JSON.stringify({ name: screenshotName })
314
+ });
315
+
316
+ if (response.ok) {
317
+ // Mark as accepted and hide the comparison
318
+ const comparison = document.querySelector(\`[data-comparison="\${screenshotName}"]\`);
319
+ if (comparison) {
320
+ comparison.style.background = '#e8f5e8';
321
+ comparison.style.border = '2px solid #4caf50';
322
+
323
+ const status = comparison.querySelector('.diff-status');
324
+ if (status) {
325
+ status.innerHTML = '✅ Accepted as new baseline';
326
+ status.style.color = '#4caf50';
327
+ }
328
+
329
+ const actions = comparison.querySelector('.comparison-actions');
330
+ if (actions) {
331
+ actions.innerHTML = '<div style="color: #4caf50; padding: 0.5rem;">✅ Screenshot accepted as new baseline</div>';
332
+ }
333
+ }
334
+
335
+ // Auto-refresh after short delay to show updated report
336
+ setTimeout(() => window.location.reload(), 2000);
337
+ } else {
338
+ throw new Error('Failed to accept baseline');
339
+ }
340
+ } catch (error) {
341
+ console.error('Error accepting baseline:', error);
342
+ if (button) {
343
+ button.disabled = false;
344
+ button.innerHTML = '✅ Accept as Baseline';
345
+ }
346
+ alert('Failed to accept baseline. Please try again.');
347
+ }
348
+ }
349
+
350
+ function rejectChanges(screenshotName) {
351
+ const comparison = document.querySelector(\`[data-comparison="\${screenshotName}"]\`);
352
+ if (comparison) {
353
+ comparison.style.background = '#fff3cd';
354
+ comparison.style.border = '2px solid #ffc107';
355
+
356
+ const status = comparison.querySelector('.diff-status');
357
+ if (status) {
358
+ status.innerHTML = '⚠️ Changes rejected - baseline unchanged';
359
+ status.style.color = '#856404';
360
+ }
361
+
362
+ const actions = comparison.querySelector('.comparison-actions');
363
+ if (actions) {
364
+ actions.innerHTML = '<div style="color: #856404; padding: 0.5rem;">⚠️ Changes rejected - baseline kept as-is</div>';
365
+ }
366
+ }
367
+ }
300
368
  </script>
301
369
  </body>
302
370
  </html>`;
@@ -331,6 +399,15 @@ document.addEventListener('DOMContentLoaded', function () {
331
399
  <button class="view-mode-btn" data-mode="side-by-side">Side by Side</button>
332
400
  </div>
333
401
 
402
+ <div class="comparison-actions">
403
+ <button class="accept-btn" onclick="acceptBaseline('${safeName}')">
404
+ ✅ Accept as Baseline
405
+ </button>
406
+ <button class="reject-btn" onclick="rejectChanges('${safeName}')">
407
+ ❌ Keep Current Baseline
408
+ </button>
409
+ </div>
410
+
334
411
  <div class="comparison-viewer">
335
412
  <!-- Overlay Mode -->
336
413
  <div class="mode-container overlay-mode" data-mode="overlay">
@@ -337,6 +337,59 @@ body {
337
337
  padding: 40px;
338
338
  }
339
339
 
340
+ /* Action buttons for accept/reject */
341
+ .comparison-actions {
342
+ display: flex;
343
+ gap: 12px;
344
+ margin: 16px 0;
345
+ padding: 16px;
346
+ background: #1e293b;
347
+ border-radius: 8px;
348
+ border: 1px solid #334155;
349
+ }
350
+
351
+ .accept-btn,
352
+ .reject-btn {
353
+ padding: 10px 16px;
354
+ border: none;
355
+ border-radius: 6px;
356
+ font-size: 14px;
357
+ font-weight: 500;
358
+ cursor: pointer;
359
+ transition: all 0.2s ease;
360
+ display: inline-flex;
361
+ align-items: center;
362
+ gap: 8px;
363
+ }
364
+
365
+ .accept-btn {
366
+ background: #059669;
367
+ color: white;
368
+ }
369
+
370
+ .accept-btn:hover {
371
+ background: #047857;
372
+ }
373
+
374
+ .accept-btn:disabled {
375
+ background: #6b7280;
376
+ cursor: not-allowed;
377
+ }
378
+
379
+ .reject-btn {
380
+ background: #dc2626;
381
+ color: white;
382
+ }
383
+
384
+ .reject-btn:hover {
385
+ background: #b91c1c;
386
+ }
387
+
388
+ .reject-btn:disabled {
389
+ background: #6b7280;
390
+ cursor: not-allowed;
391
+ }
392
+
340
393
  @media (max-width: 768px) {
341
394
  .container {
342
395
  padding: 10px;
@@ -352,4 +405,7 @@ body {
352
405
  grid-template-columns: 1fr;
353
406
  gap: 15px;
354
407
  }
408
+ .comparison-actions {
409
+ flex-direction: column;
410
+ }
355
411
  }
@@ -45,14 +45,17 @@ export class ScreenshotServer extends BaseService {
45
45
  image,
46
46
  properties
47
47
  } = body;
48
- if (!buildId || !name || !image) {
48
+ if (!name || !image) {
49
49
  res.statusCode = 400;
50
50
  res.end(JSON.stringify({
51
- error: 'buildId, name, and image are required'
51
+ error: 'name and image are required'
52
52
  }));
53
53
  return;
54
54
  }
55
- await this.buildManager.addScreenshot(buildId, {
55
+
56
+ // Use default buildId if none provided
57
+ const effectiveBuildId = buildId || 'default';
58
+ await this.buildManager.addScreenshot(effectiveBuildId, {
56
59
  name,
57
60
  image,
58
61
  properties
@@ -7,7 +7,6 @@ import { BaseService } from './base-service.js';
7
7
  import { createHttpServer } from '../server/http-server.js';
8
8
  import { createTddHandler } from '../server/handlers/tdd-handler.js';
9
9
  import { createApiHandler } from '../server/handlers/api-handler.js';
10
- import { EventEmitter } from 'events';
11
10
  export class ServerManager extends BaseService {
12
11
  constructor(config, logger) {
13
12
  super(config, {
@@ -15,7 +14,6 @@ export class ServerManager extends BaseService {
15
14
  });
16
15
  this.httpServer = null;
17
16
  this.handler = null;
18
- this.emitter = null;
19
17
  }
20
18
  async start(buildId = null, tddMode = false, setBaseline = false) {
21
19
  this.buildId = buildId;
@@ -24,19 +22,15 @@ export class ServerManager extends BaseService {
24
22
  return super.start();
25
23
  }
26
24
  async onStart() {
27
- this.emitter = new EventEmitter();
28
25
  const port = this.config?.server?.port || 47392;
29
26
  if (this.tddMode) {
30
27
  this.handler = createTddHandler(this.config, process.cwd(), this.config?.baselineBuildId, this.config?.baselineComparisonId, this.setBaseline);
31
28
  await this.handler.initialize();
32
- if (this.buildId) {
33
- this.handler.registerBuild(this.buildId);
34
- }
35
29
  } else {
36
30
  const apiService = await this.createApiService();
37
31
  this.handler = createApiHandler(apiService);
38
32
  }
39
- this.httpServer = createHttpServer(port, this.handler, this.emitter);
33
+ this.httpServer = createHttpServer(port, this.handler);
40
34
  if (this.httpServer) {
41
35
  await this.httpServer.start();
42
36
  }
@@ -70,9 +64,8 @@ export class ServerManager extends BaseService {
70
64
  // Expose server interface for compatibility
71
65
  get server() {
72
66
  return {
73
- emitter: this.emitter,
74
67
  getScreenshotCount: buildId => this.handler?.getScreenshotCount?.(buildId) || 0,
75
- finishBuild: buildId => this.handler?.finishBuild?.(buildId)
68
+ finishBuild: buildId => this.httpServer?.finishBuild?.(buildId)
76
69
  };
77
70
  }
78
71
  }