VISHAL18for4 commited on
Commit
7a5f972
·
verified ·
1 Parent(s): e0ae649

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +123 -405
server.js CHANGED
@@ -1,461 +1,179 @@
1
- // ============================================================
2
- // 1. DOM References
3
- // ============================================================
4
- const video = document.getElementById('webcam');
5
- const canvas = document.getElementById('drawCanvas');
6
- const ctx = canvas.getContext('2d');
7
- const statusDiv = document.getElementById('status');
8
- const gestureIndicator = document.getElementById('gestureIndicator');
9
- const fpsSpan = document.getElementById('fpsCounter');
10
- const savedSpan = document.getElementById('savedCounter');
11
 
12
- const brushColorInput = document.getElementById('brushColor');
13
- const sizeDisplay = document.getElementById('sizeDisplay');
14
- const eraserBtn = document.getElementById('eraserToggle');
15
- const clearBtn = document.getElementById('clearCanvas');
16
- const snapshotBtn = document.getElementById('snapshotBtn');
17
- const toggleCamBtn = document.getElementById('toggleCamera');
18
- const sizeDownBtn = document.getElementById('brushSizeDown');
19
- const sizeUpBtn = document.getElementById('brushSizeUp');
20
 
21
  // ============================================================
22
- // 2. On‑Screen Console (for mobile debugging)
23
  // ============================================================
24
- const consoleDiv = document.createElement('div');
25
- consoleDiv.id = 'mobileConsole';
26
- consoleDiv.style.cssText = `
27
- position: fixed; bottom: 80px; left: 10px; right: 10px;
28
- background: rgba(0,0,0,0.85); color: #0f0;
29
- font-family: monospace; font-size: 11px;
30
- padding: 8px; border-radius: 6px;
31
- max-height: 120px; overflow-y: auto;
32
- z-index: 9999; border: 1px solid #333;
33
- display: none; word-break: break-all;
34
- `;
35
- document.body.appendChild(consoleDiv);
36
 
37
- function mobileLog(msg) {
38
- consoleDiv.style.display = 'block';
39
- const entry = document.createElement('div');
40
- entry.textContent = '> ' + msg;
41
- consoleDiv.appendChild(entry);
42
- if (consoleDiv.children.length > 20) {
43
- consoleDiv.removeChild(consoleDiv.firstChild);
44
  }
45
- consoleDiv.scrollTop = consoleDiv.scrollHeight;
46
- // Also log to real console if available
47
- console.log('[MobileLog]', msg);
48
- }
49
-
50
- // Toggle console with double‑tap on status bar
51
- let consoleTapCount = 0;
52
- statusDiv.addEventListener('dblclick', () => {
53
- consoleDiv.style.display = consoleDiv.style.display === 'none' ? 'block' : 'none';
54
  });
55
 
56
  // ============================================================
57
- // 3. State Variables
58
  // ============================================================
59
- let drawing = false;
60
- let lastX = null;
61
- let lastY = null;
62
- let currentColor = '#FF4500';
63
- let brushSize = 5;
64
- let eraserMode = false;
65
- let cameraVisible = true;
66
- let savedCount = 0;
67
- let frameCount = 0;
68
- let lastFpsUpdate = performance.now();
69
- let handLandmarker = null;
70
- let cameraStream = null;
71
- let isModelReady = false;
72
- let canvasWidth = 0;
73
- let canvasHeight = 0;
74
- let modelLoadAttempts = 0;
75
- const MAX_MODEL_ATTEMPTS = 3;
76
 
77
- // ============================================================
78
- // 4. Canvas Resize
79
- // ============================================================
80
- function resizeCanvas() {
81
- const rect = canvas.parentElement.getBoundingClientRect();
82
- canvas.width = rect.width;
83
- canvas.height = rect.height;
84
- canvasWidth = canvas.width;
85
- canvasHeight = canvas.height;
86
  }
87
- window.addEventListener('resize', resizeCanvas);
88
 
89
  // ============================================================
90
- // 5. Admin Panel Trigger – Mobile Taps + Keyboard '3'
91
  // ============================================================
92
- let adminTriggerCount = 0;
93
- let adminTriggerTimer = null;
94
 
95
- function openAdminPanel() {
96
- // Open in same tab by changing location
97
- window.location.href = '/admin.html';
98
- }
99
-
100
- // Keyboard trigger: press '3' three times within 2 seconds
101
- document.addEventListener('keydown', (e) => {
102
- if (e.key === '3') {
103
- e.preventDefault();
104
- adminTriggerCount++;
105
- clearTimeout(adminTriggerTimer);
106
- adminTriggerTimer = setTimeout(() => { adminTriggerCount = 0; }, 2000);
107
- mobileLog(`Key '3' pressed (${adminTriggerCount}/3)`);
108
-
109
- if (adminTriggerCount >= 3) {
110
- adminTriggerCount = 0;
111
- mobileLog('🔐 Opening admin panel...');
112
- openAdminPanel();
113
- }
114
  }
115
- });
116
-
117
- // Screen tap trigger: tap 3 times within 2 seconds on the canvas or gesture indicator
118
- function setupTapTrigger(element) {
119
- let tapCount = 0;
120
- let tapTimer = null;
121
 
122
- element.addEventListener('click', (e) => {
123
- // Ignore clicks on buttons and toolbar
124
- if (e.target.closest('button') || e.target.closest('.toolbar') || e.target.closest('header')) {
125
- return;
126
- }
127
-
128
- tapCount++;
129
- clearTimeout(tapTimer);
130
- tapTimer = setTimeout(() => { tapCount = 0; }, 2000);
131
- mobileLog(`Tap (${tapCount}/3)`);
132
-
133
- if (tapCount >= 3) {
134
- tapCount = 0;
135
- mobileLog('🔐 Opening admin panel via tap...');
136
- openAdminPanel();
137
  }
138
- });
139
- }
140
-
141
- // Attach tap trigger to canvas and gesture indicator
142
- setupTapTrigger(canvas);
143
- setupTapTrigger(gestureIndicator);
144
-
145
- // Also trigger via triple‑tap on the status bar (for debugging)
146
- let statusTapCount = 0;
147
- let statusTapTimer = null;
148
- statusDiv.addEventListener('click', () => {
149
- statusTapCount++;
150
- clearTimeout(statusTapTimer);
151
- statusTapTimer = setTimeout(() => { statusTapCount = 0; }, 2000);
152
- if (statusTapCount >= 3) {
153
- statusTapCount = 0;
154
- mobileLog('🔐 Opening admin via status tap...');
155
- openAdminPanel();
156
  }
157
  });
158
 
159
  // ============================================================
160
- // 6. MediaPipe Hand Landmarker with Detailed Logging
161
  // ============================================================
162
- async function initHandLandmarker() {
163
  try {
164
- mobileLog(`⏳ Loading model (attempt ${modelLoadAttempts + 1})...`);
165
- statusDiv.textContent = '⏳ Loading hand model...';
166
 
167
- // Load WASM
168
- mobileLog('Loading WASM...');
169
- const vision = await FilesetResolver.forVisionTasks(
170
- 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.8/wasm'
171
- );
172
- mobileLog('✅ WASM loaded');
173
-
174
- // Try local file first, then CDN fallbacks
175
- const modelSources = [
176
- '/hand_landmarker.task',
177
- 'https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task',
178
- 'https://huggingface.co/spaces/GoogleMediaPipe/hand_landmarker/resolve/main/hand_landmarker.task'
179
- ];
180
 
181
- let lastError = null;
182
- for (let i = 0; i < modelSources.length; i++) {
183
- try {
184
- mobileLog(`Trying source ${i+1}/${modelSources.length}: ${modelSources[i].substring(0, 50)}...`);
185
- statusDiv.textContent = `⏳ Loading model (${i+1}/${modelSources.length})...`;
186
-
187
- handLandmarker = await HandLandmarker.createFromOptions(vision, {
188
- baseOptions: {
189
- modelAssetPath: modelSources[i],
190
- delegate: 'GPU'
191
- },
192
- runningMode: 'VIDEO',
193
- numHands: 1
194
- });
195
-
196
- // Success
197
- isModelReady = true;
198
- mobileLog(`✅ Model loaded from source ${i+1}`);
199
- statusDiv.textContent = '✅ Model ready · Draw with index finger';
200
- return;
201
- } catch (err) {
202
- lastError = err;
203
- mobileLog(`❌ Source ${i+1} failed: ${err.message || 'unknown error'}`);
204
- console.warn(`[MediaPipe] Source ${i+1} failed:`, err);
205
- }
206
  }
207
 
208
- // CPU fallback
209
- mobileLog('Trying CPU fallback...');
210
- try {
211
- handLandmarker = await HandLandmarker.createFromOptions(vision, {
212
- baseOptions: {
213
- modelAssetPath: modelSources[0],
214
- delegate: 'CPU'
215
- },
216
- runningMode: 'VIDEO',
217
- numHands: 1
218
- });
219
- isModelReady = true;
220
- mobileLog('✅ Model loaded (CPU mode)');
221
- statusDiv.textContent = '✅ Model ready (CPU) · Draw with index finger';
222
- return;
223
- } catch (cpuErr) {
224
- mobileLog(` CPU fallback failed: ${cpuErr.message}`);
 
 
 
 
 
 
 
225
  }
226
 
227
- throw lastError || new Error('All model sources failed');
 
 
 
 
228
 
229
  } catch (err) {
230
- mobileLog(`❌ FATAL: ${err.message || 'unknown error'}`);
231
- console.error('[MediaPipe] Fatal error:', err);
232
- statusDiv.textContent = '❌ Model load failed. Tap here to retry.';
233
- statusDiv.style.cursor = 'pointer';
234
- statusDiv.onclick = () => {
235
- modelLoadAttempts++;
236
- if (modelLoadAttempts < MAX_MODEL_ATTEMPTS) {
237
- initHandLandmarker();
238
- } else {
239
- statusDiv.textContent = '❌ Max retries. Please refresh.';
240
- mobileLog('❌ Max retries reached');
241
- }
242
- };
243
  }
244
- }
245
 
246
  // ============================================================
247
- // 7. Camera Setup
248
  // ============================================================
249
- async function startCamera() {
250
  try {
251
- mobileLog('📷 Requesting camera...');
252
- cameraStream = await navigator.mediaDevices.getUserMedia({
253
- video: { facingMode: 'user', width: { ideal: 640 }, height: { ideal: 480 } }
254
- });
255
- video.srcObject = cameraStream;
256
- await video.play();
257
- mobileLog('✅ Camera ready');
258
- statusDiv.textContent = '📷 Camera ready · Loading model...';
259
- initHandLandmarker();
260
- requestAnimationFrame(drawLoop);
261
  } catch (err) {
262
- mobileLog(`❌ Camera error: ${err.message}`);
263
- console.error('[Camera] Error:', err);
264
- statusDiv.textContent = '❌ Camera access denied. Please allow and refresh.';
265
- alert('Camera access is required. Please allow and refresh.');
266
  }
267
- }
268
 
269
  // ============================================================
270
- // 8. Core Drawing Loop
271
  // ============================================================
272
- function drawLoop(timestamp) {
273
- frameCount++;
274
- if (timestamp - lastFpsUpdate >= 1000) {
275
- fpsSpan.textContent = `${frameCount} FPS`;
276
- frameCount = 0;
277
- lastFpsUpdate = timestamp;
278
- }
279
-
280
- if (video.readyState >= 2) {
281
- ctx.drawImage(video, 0, 0, canvasWidth, canvasHeight);
282
- } else {
283
- ctx.fillStyle = '#1a1a2e';
284
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
285
  }
286
-
287
- if (isModelReady && handLandmarker && video.readyState >= 2) {
 
 
288
  try {
289
- const results = handLandmarker.detectForVideo(video, performance.now());
290
-
291
- if (results.landmarks && results.landmarks.length > 0) {
292
- const lm = results.landmarks[0];
293
- const tip = lm[8];
294
- const mcp = lm[5];
295
-
296
- const isIndexUp = (tip.y < mcp.y - 0.02);
297
-
298
- if (isIndexUp) {
299
- gestureIndicator.className = 'gesture-on';
300
- gestureIndicator.textContent = '✋';
301
- } else {
302
- gestureIndicator.className = 'gesture-off';
303
- gestureIndicator.textContent = '✊';
304
- }
305
-
306
- const x = tip.x * canvasWidth;
307
- const y = tip.y * canvasHeight;
308
-
309
- if (isIndexUp) {
310
- if (!drawing) {
311
- drawing = true;
312
- lastX = x;
313
- lastY = y;
314
- } else {
315
- if (lastX !== null && lastY !== null) {
316
- ctx.beginPath();
317
- ctx.moveTo(lastX, lastY);
318
- ctx.lineTo(x, y);
319
- ctx.strokeStyle = eraserMode ? '#1a1a2e' : currentColor;
320
- ctx.lineWidth = eraserMode ? brushSize * 2 : brushSize;
321
- ctx.lineCap = 'round';
322
- ctx.lineJoin = 'round';
323
- ctx.stroke();
324
- }
325
- lastX = x;
326
- lastY = y;
327
- }
328
- } else {
329
- drawing = false;
330
- lastX = null;
331
- lastY = null;
332
- }
333
-
334
- // Cursor
335
- ctx.beginPath();
336
- ctx.arc(x, y, 4, 0, 2 * Math.PI);
337
- ctx.fillStyle = eraserMode ? '#ff6666' : currentColor;
338
- ctx.fill();
339
- ctx.strokeStyle = '#fff';
340
- ctx.lineWidth = 1;
341
- ctx.stroke();
342
-
343
- } else {
344
- gestureIndicator.className = 'gesture-off';
345
- gestureIndicator.textContent = '🚫';
346
- drawing = false;
347
- lastX = null;
348
- lastY = null;
349
- }
350
  } catch (err) {
351
- // silent
352
- }
353
- }
354
-
355
- requestAnimationFrame(drawLoop);
356
- }
357
-
358
- // ============================================================
359
- // 9. Snapshot Capture
360
- // ============================================================
361
- async function captureSnapshot() {
362
- try {
363
- const tempCanvas = document.createElement('canvas');
364
- tempCanvas.width = canvasWidth;
365
- tempCanvas.height = canvasHeight;
366
- const tempCtx = tempCanvas.getContext('2d');
367
-
368
- if (video.readyState >= 2) {
369
- tempCtx.drawImage(video, 0, 0, canvasWidth, canvasHeight);
370
- }
371
- tempCtx.drawImage(canvas, 0, 0);
372
-
373
- const dataURL = tempCanvas.toDataURL('image/png');
374
- const base64 = dataURL.split(',')[1];
375
-
376
- const response = await fetch('/api/snapshot', {
377
- method: 'POST',
378
- headers: { 'Content-Type': 'application/json' },
379
- body: JSON.stringify({ image: base64 })
380
- });
381
-
382
- const result = await response.json();
383
- if (result.success) {
384
- savedCount++;
385
- savedSpan.textContent = `Saved: ${savedCount}`;
386
- statusDiv.textContent = `✅ Snapshot saved: ${result.filename}`;
387
- mobileLog(`📸 Saved: ${result.filename}`);
388
- canvas.style.transition = 'opacity 0.1s';
389
- canvas.style.opacity = '0.7';
390
- setTimeout(() => { canvas.style.opacity = '1'; }, 150);
391
- } else {
392
- statusDiv.textContent = `❌ Save failed: ${result.error}`;
393
- mobileLog(`❌ Save failed: ${result.error}`);
394
- }
395
- } catch (err) {
396
- mobileLog(`❌ Save error: ${err.message}`);
397
- console.error('[Snapshot] Error:', err);
398
- statusDiv.textContent = '❌ Network error while saving snapshot.';
399
- }
400
- }
401
-
402
- // ============================================================
403
- // 10. UI Event Bindings
404
- // ============================================================
405
- brushColorInput.addEventListener('input', (e) => {
406
- currentColor = e.target.value;
407
- eraserMode = false;
408
- eraserBtn.classList.remove('active');
409
- });
410
-
411
- sizeDownBtn.addEventListener('click', () => {
412
- brushSize = Math.max(1, brushSize - 1);
413
- sizeDisplay.textContent = brushSize;
414
- });
415
- sizeUpBtn.addEventListener('click', () => {
416
- brushSize = Math.min(50, brushSize + 1);
417
- sizeDisplay.textContent = brushSize;
418
- });
419
-
420
- eraserBtn.addEventListener('click', () => {
421
- eraserMode = !eraserMode;
422
- eraserBtn.classList.toggle('active');
423
- statusDiv.textContent = eraserMode ? '🧹 Eraser mode ON' : '🎨 Drawing mode';
424
- });
425
-
426
- clearBtn.addEventListener('click', () => {
427
- if (confirm('Clear all drawings?')) {
428
- drawing = false;
429
- lastX = null;
430
- lastY = null;
431
- statusDiv.textContent = '🗑️ Canvas cleared';
432
- if (video.readyState >= 2) {
433
- ctx.drawImage(video, 0, 0, canvasWidth, canvasHeight);
434
  }
 
 
435
  }
436
  });
437
 
438
- snapshotBtn.addEventListener('click', captureSnapshot);
439
-
440
- toggleCamBtn.addEventListener('click', () => {
441
- cameraVisible = !cameraVisible;
442
- video.style.display = cameraVisible ? 'block' : 'none';
443
- toggleCamBtn.classList.toggle('active');
444
- });
445
-
446
  // ============================================================
447
- // 11. Keyboard Shortcuts (including 's' for save)
448
  // ============================================================
449
- document.addEventListener('keydown', (e) => {
450
- if (e.key === 's' || e.key === 'S') captureSnapshot();
451
- if (e.key === 'e' || e.key === 'E') eraserBtn.click();
452
- if (e.key === 'c' || e.key === 'C') clearBtn.click();
453
  });
454
 
455
  // ============================================================
456
- // 12. Initialization
457
  // ============================================================
458
- resizeCanvas();
459
- startCamera();
460
- mobileLog('🚀 App initialized. Tap canvas 3x or press 3 three times for admin.');
461
- console.log('[Gesture Draw] App initialized.');
 
 
1
+ const express = require('express');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const cors = require('cors');
 
 
 
 
 
 
5
 
6
+ const app = express();
7
+ const PORT = process.env.PORT || 7860;
 
 
 
 
 
 
8
 
9
  // ============================================================
10
+ // 1. Middleware
11
  // ============================================================
12
+ app.use(cors());
13
+ app.use(express.json({ limit: '10mb' }));
 
 
 
 
 
 
 
 
 
 
14
 
15
+ // Force correct MIME type for MediaPipe .task files
16
+ app.use((req, res, next) => {
17
+ if (req.url.endsWith('.task')) {
18
+ res.setHeader('Content-Type', 'application/octet-stream');
19
+ res.setHeader('Content-Disposition', 'attachment; filename="hand_landmarker.task"');
 
 
20
  }
21
+ next();
 
 
 
 
 
 
 
 
22
  });
23
 
24
  // ============================================================
25
+ // 2. Snapshots Directory Setup
26
  // ============================================================
27
+ const SNAPSHOT_DIR = path.join(__dirname, 'snapshots');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
+ if (!fs.existsSync(SNAPSHOT_DIR)) {
30
+ fs.mkdirSync(SNAPSHOT_DIR, { recursive: true });
31
+ console.log(`[SERVER] Created snapshots directory at ${SNAPSHOT_DIR}`);
 
 
 
 
 
 
32
  }
 
33
 
34
  // ============================================================
35
+ // 3. Serve Static Frontend Files
36
  // ============================================================
37
+ app.use(express.static(path.join(__dirname, 'public')));
 
38
 
39
+ // ============================================================
40
+ // 4. Snapshot Binary Endpoint (bypasses static route restrictions)
41
+ // ============================================================
42
+ app.get('/snapshots/:filename', (req, res) => {
43
+ const filename = req.params.filename;
44
+
45
+ // Security: prevent directory traversal
46
+ if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
47
+ console.warn(`[SERVER] Blocked malicious filename: ${filename}`);
48
+ return res.status(400).send('Invalid filename');
 
 
 
 
 
 
 
 
 
49
  }
 
 
 
 
 
 
50
 
51
+ const filePath = path.join(SNAPSHOT_DIR, filename);
52
+
53
+ if (fs.existsSync(filePath)) {
54
+ try {
55
+ const data = fs.readFileSync(filePath);
56
+ res.setHeader('Content-Type', 'image/png');
57
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
58
+ res.send(data);
59
+ console.log(`[SERVER] Served snapshot: ${filename} (${data.length} bytes)`);
60
+ } catch (err) {
61
+ console.error(`[SERVER] Error reading snapshot ${filename}:`, err);
62
+ res.status(500).send('Error reading file');
 
 
 
63
  }
64
+ } else {
65
+ console.log(`[SERVER] Snapshot not found: ${filename}`);
66
+ res.status(404).send('Snapshot not found');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  }
68
  });
69
 
70
  // ============================================================
71
+ // 5. POST /api/snapshot Save New Snapshot
72
  // ============================================================
73
+ app.post('/api/snapshot', (req, res) => {
74
  try {
75
+ const { image } = req.body;
 
76
 
77
+ if (!image) {
78
+ return res.status(400).json({ success: false, error: 'No image data provided' });
79
+ }
 
 
 
 
 
 
 
 
 
 
80
 
81
+ if (typeof image !== 'string' || image.length < 100) {
82
+ return res.status(400).json({ success: false, error: 'Invalid image data format' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  }
84
 
85
+ const buffer = Buffer.from(image, 'base64');
86
+
87
+ const now = new Date();
88
+ const dateStr =
89
+ now.getFullYear() + '-' +
90
+ String(now.getMonth() + 1).padStart(2, '0') + '-' +
91
+ String(now.getDate()).padStart(2, '0') + '_' +
92
+ String(now.getHours()).padStart(2, '0') + '-' +
93
+ String(now.getMinutes()).padStart(2, '0') + '-' +
94
+ String(now.getSeconds()).padStart(2, '0') + '_' +
95
+ String(now.getMilliseconds()).padStart(3, '0');
96
+
97
+ const filename = `snapshot_${dateStr}.png`;
98
+ const filePath = path.join(SNAPSHOT_DIR, filename);
99
+
100
+ fs.writeFileSync(filePath, buffer);
101
+ console.log(`[SERVER] Snapshot saved: ${filename} (${buffer.length} bytes) at ${filePath}`);
102
+
103
+ if (fs.existsSync(filePath)) {
104
+ const stats = fs.statSync(filePath);
105
+ console.log(`[SERVER] Verification: file exists, size ${stats.size} bytes`);
106
+ } else {
107
+ console.error(`[SERVER] Verification FAILED: file not found after write`);
108
+ return res.status(500).json({ success: false, error: 'Write verification failed' });
109
  }
110
 
111
+ res.json({
112
+ success: true,
113
+ path: `/snapshots/${filename}`,
114
+ filename: filename
115
+ });
116
 
117
  } catch (err) {
118
+ console.error('[SERVER] Save error:', err);
119
+ res.status(500).json({ success: false, error: 'Internal server error: ' + err.message });
 
 
 
 
 
 
 
 
 
 
 
120
  }
121
+ });
122
 
123
  // ============================================================
124
+ // 6. GET /api/snapshots/list – List All Snapshots (for Admin)
125
  // ============================================================
126
+ app.get('/api/snapshots/list', (req, res) => {
127
  try {
128
+ const files = fs.readdirSync(SNAPSHOT_DIR)
129
+ .filter(f => f.endsWith('.png'))
130
+ .sort();
131
+
132
+ console.log(`[SERVER] Listed ${files.length} snapshots`);
133
+ res.json({ success: true, files });
 
 
 
 
134
  } catch (err) {
135
+ console.error('[SERVER] List error:', err);
136
+ res.status(500).json({ success: false, error: err.message });
 
 
137
  }
138
+ });
139
 
140
  // ============================================================
141
+ // 7. DELETE /api/snapshots/:filename – Delete a Snapshot
142
  // ============================================================
143
+ app.delete('/api/snapshots/:filename', (req, res) => {
144
+ const filename = req.params.filename;
145
+
146
+ if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
147
+ return res.status(400).json({ success: false, error: 'Invalid filename' });
 
 
 
 
 
 
 
 
148
  }
149
+
150
+ const filePath = path.join(SNAPSHOT_DIR, filename);
151
+
152
+ if (fs.existsSync(filePath)) {
153
  try {
154
+ fs.unlinkSync(filePath);
155
+ console.log(`[SERVER] Deleted snapshot: ${filename}`);
156
+ res.json({ success: true, message: 'Deleted' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  } catch (err) {
158
+ res.status(500).json({ success: false, error: err.message });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  }
160
+ } else {
161
+ res.status(404).json({ success: false, error: 'File not found' });
162
  }
163
  });
164
 
 
 
 
 
 
 
 
 
165
  // ============================================================
166
+ // 8. Health Check
167
  // ============================================================
168
+ app.get('/health', (req, res) => {
169
+ res.status(200).send('OK');
 
 
170
  });
171
 
172
  // ============================================================
173
+ // 9. Start Server
174
  // ============================================================
175
+ app.listen(PORT, () => {
176
+ console.log(`[SERVER] Running on port ${PORT}`);
177
+ console.log(`[SERVER] Snapshots directory: ${SNAPSHOT_DIR}`);
178
+ console.log(`[SERVER] Serving static files from: ${path.join(__dirname, 'public')}`);
179
+ });