import express from 'express';
import https from 'https';
import { Readable } from 'node:stream';
import path from 'path';
import fs from 'fs';
import fsPromises from 'fs/promises';
import { fileURLToPath } from 'url';
import process from 'process';
import { spawn } from 'child_process';
import { WebcamClient } from './webcamClient.js';
import { assignByZRange, removeMarkerAssignment, alignSetToMeasured, assignMarkerId, adoptXAxis, assignFixedMarkersToLink, setJointOriginYZ } from './editRobot.js';
import { runHoming } from './homingOrchestrator.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const app = express();
app.use(express.json({ limit: '20mb' }));

const PORT = parseInt(process.env.PORT || process.env.HTTPS_PORT || '2093', 10);
const publicDir = path.join(__dirname, '..', 'public');
const snapshotsDir = path.join(publicDir, 'snapshots');
const WEBCAM_URL      = process.env.WEBCAM_URL      || '';
const BODYTRACKER_URL = process.env.BODYTRACKER_URL || '';
const ROBOT_URL       = process.env.ROBOT_URL       || '';
const HTTPS_KEY_PATH = process.env.HTTPS_KEY_PATH || path.join(__dirname, '..', 'https', 'localhost.key');
const HTTPS_CERT_PATH = process.env.HTTPS_CERT_PATH || path.join(__dirname, '..', 'https', 'localhost.pem');
const HTTPS_PASSPHRASE = process.env.HTTPS_PASSPHRASE || 'abcd';

app.use(express.static(publicDir));

app.get('/api/health', (req, res) => {
  res.json({ ok: true, mode: 'backend-proxy', webcamUrl: WEBCAM_URL || null, bodyTrackerUrl: BODYTRACKER_URL || null });
});

// ── WebCam-Proxy ─────────────────────────────────────────────────────────────

/** Kameraliste mit Metadaten (inkl. calibrationUrl falls Kalibrierung vorhanden). */
app.get('/api/webcam/cameras', async (req, res) => {
  if (!WEBCAM_URL) return res.status(501).json({ error: 'WEBCAM_URL ist nicht konfiguriert' });
  try {
    const wc = new WebcamClient(WEBCAM_URL);
    const data = await wc.getCameras();
    return res.json(data);
  } catch (err) {
    console.error('webcam/cameras error:', err);
    return res.status(502).json({ error: 'WebCam-Fehler', details: String(err) });
  }
});

/**
 * HD-JPEG einer Kamera (per Default hires).
 * Streamt die JPEG-Antwort direkt durch — kein Buffering im Backend.
 * Query-Parameter: ?hires=false für Live-Auflösung.
 */
app.get('/api/webcam/snapshot/:id', async (req, res) => {
  if (!WEBCAM_URL) return res.status(501).json({ error: 'WEBCAM_URL ist nicht konfiguriert' });
  const hires = req.query.hires !== 'false';
  try {
    const wc = new WebcamClient(WEBCAM_URL);
    const upstream = await wc.getSnapshot(req.params.id, hires);

    // Relevante Response-Header durchreichen
    res.setHeader('Content-Type', upstream.headers.get('content-type') || 'image/jpeg');
    res.setHeader('Cache-Control', 'no-store');
    for (const header of ['x-camera-id', 'x-frame-width', 'x-timestamp', 'content-length']) {
      const val = upstream.headers.get(header);
      if (val) res.setHeader(header, val);
    }

    const nodeStream = Readable.fromWeb(upstream.body);
    nodeStream.on('error', (err) => {
      console.error(`webcam/snapshot/${req.params.id} stream error:`, err);
      if (!res.headersSent) res.status(502).json({ error: 'Stream-Fehler' });
    });
    nodeStream.pipe(res);
  } catch (err) {
    console.error(`webcam/snapshot/${req.params.id} error:`, err);
    if (!res.headersSent) res.status(502).json({ error: 'WebCam-Fehler', details: String(err) });
  }
});

async function findLatestSnapshotFile() {
  const files = await fsPromises.readdir(snapshotsDir);
  const entries = await Promise.all(
    files
      .filter((name) => name.endsWith('.csv'))
      .map(async (name) => ({
        name,
        mtime: (await fsPromises.stat(path.join(snapshotsDir, name))).mtime.valueOf()
      }))
  );
  if (entries.length === 0) return null;
  entries.sort((a, b) => b.mtime - a.mtime);
  return entries[0].name;
}

app.get('/api/latest-snapshot', async (req, res) => {
  try {
    if (WEBCAM_URL) {
      const url = new URL('/api/latest-snapshot', WEBCAM_URL).toString();
      const fetchRes = await fetch(url);
      const contentType = fetchRes.headers.get('content-type') || '';

      if (!fetchRes.ok) {
        const text = await fetchRes.text();
        return res.status(fetchRes.status).type('text/plain').send(text);
      }

      if (contentType.includes('application/json')) {
        const body = await fetchRes.json();
        return res.json(body);
      }

      const text = await fetchRes.text();
      return res.json({ filename: 'latest.csv', mtime: new Date().toISOString(), content: text });
    }

    const latestFile = await findLatestSnapshotFile();
    if (!latestFile) {
      return res.status(404).json({ error: 'Keine Snapshot-CSV-Datei gefunden' });
    }

    const baseName = path.basename(latestFile, path.extname(latestFile));
    const csvPath = path.join(snapshotsDir, latestFile);
    const jsonPath = path.join(snapshotsDir, `${baseName}.json`);
    const imagePath = path.join(snapshotsDir, `${baseName}_annotated.jpg`);
    const imagePath2 = path.join(snapshotsDir, `${baseName}_annotated2.jpg`);

    const content = await fsPromises.readFile(csvPath, 'utf8');
    const result = { filename: latestFile, mtime: (await fsPromises.stat(csvPath)).mtime.toISOString(), content };

    try {
      result.jsonFile = { filename: `${baseName}.json`, content: await fsPromises.readFile(jsonPath, 'utf8') };
    } catch {}

    try {
      const jpg = await fsPromises.readFile(imagePath);
      result.imageFile = {
        filename: path.basename(imagePath),
        mimeType: 'image/jpeg',
        contentBase64: jpg.toString('base64')
      };
    } catch {}

    try {
      const jpg2 = await fsPromises.readFile(imagePath2);
      result.image2 = {
        filename: path.basename(imagePath2),
        mimeType: 'image/jpeg',
        contentBase64: jpg2.toString('base64')
      };
    } catch {}

    return res.json(result);
  } catch (err) {
    console.error('latest-snapshot error:', err);
    return res.status(500).json({ error: 'Fehler beim Laden des Snapshots', details: String(err) });
  }
});

app.post('/api/estimate', async (req, res) => {
  if (!BODYTRACKER_URL) {
    return res.status(501).json({ error: 'BODYTRACKER_URL ist nicht konfiguriert' });
  }

  try {
    const { imageFile, image2, robotIntrinsics } = req.body;
    const formData = new FormData();

    if (imageFile?.contentBase64) {
      const buffer = Buffer.from(imageFile.contentBase64, 'base64');
      formData.append('images', new Blob([buffer], { type: imageFile.mimeType || 'image/jpeg' }), imageFile.filename || 'snapshot.jpg');
    }

    if (image2?.contentBase64) {
      const buffer2 = Buffer.from(image2.contentBase64, 'base64');
      formData.append('images', new Blob([buffer2], { type: image2.mimeType || 'image/jpeg' }), image2.filename || 'snapshot2.jpg');
    }

    if (robotIntrinsics) {
      formData.append('intrinsics', new Blob([JSON.stringify(robotIntrinsics)], { type: 'application/json' }), 'intrinsics.json');
    }

    const estimateUrl = new URL('/v1/estimate', BODYTRACKER_URL).toString();
    const fetchRes = await fetch(estimateUrl, { method: 'POST', body: formData });

    if (!fetchRes.ok) {
      const message = await fetchRes.text();
      return res.status(fetchRes.status).json({ error: 'BodyTracker-Fehler', details: message });
    }

    const body = await fetchRes.json();
    return res.json(body);
  } catch (err) {
    console.error('estimate error:', err);
    return res.status(500).json({ error: 'Fehler beim Aufruf des BodyTracker', details: String(err) });
  }
});

// ── Kalibrierung ─────────────────────────────────────────────────────────────

const calibDataDir = path.join(__dirname, '..', 'data', 'calibration');

/** Timestamp-String im Format YYYYMMDD_HHmmss */
function makeTimestamp() {
  const now = new Date();
  const p = (n, l = 2) => String(n).padStart(l, '0');
  return `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}_${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
}

/** Neueste Kalibrierungs-Session (Verzeichnisname) oder null */
async function findLatestCalibSession() {
  try {
    await fsPromises.access(calibDataDir);
    const entries = await fsPromises.readdir(calibDataDir, { withFileTypes: true });
    const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort().reverse();
    return dirs[0] ?? null;
  } catch {
    return null;
  }
}

/**
 * Sucht die neueste Kalibrierungs-Session, die eine NPZ für die angegebene Kamera enthält.
 * Gibt { session, npzPath } zurück oder null wenn keine gefunden.
 */
async function findLatestNpzForCamera(camId) {
  try {
    await fsPromises.access(calibDataDir);
    const entries = await fsPromises.readdir(calibDataDir, { withFileTypes: true });
    const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort().reverse();
    for (const dir of dirs) {
      const npzPath = path.join(calibDataDir, dir, `${camId}_calibration.npz`);
      try {
        await fsPromises.access(npzPath);
        return { session: dir, npzPath };
      } catch {}
    }
    return null;
  } catch {
    return null;
  }
}

/** Liest meta.json einer Session */
async function readCalibMeta(sessionName) {
  try {
    const raw = await fsPromises.readFile(path.join(calibDataDir, sessionName, 'meta.json'), 'utf8');
    return JSON.parse(raw);
  } catch {
    return null;
  }
}

/** Schreibt meta.json einer Session */
async function writeCalibMeta(sessionName, meta) {
  await fsPromises.writeFile(
    path.join(calibDataDir, sessionName, 'meta.json'),
    JSON.stringify(meta, null, 2),
    'utf8'
  );
}

/**
 * Holt Snapshots aller verfügbaren Kameras und speichert sie im Session-Verzeichnis.
 * Dateiname: {cameraId}_{setNr}.jpg  (setNr = 001, 002, …)
 */
async function capturePhotos(sessionName) {
  if (!WEBCAM_URL) throw new Error('WEBCAM_URL ist nicht konfiguriert – keine Kameras erreichbar');

  const wc = new WebcamClient(WEBCAM_URL);
  const data = await wc.getCameras();
  const cameraIds = (data.cameras ?? []).map(c => c.id);
  if (cameraIds.length === 0) throw new Error('Keine Kameras vom WebCam-Service gemeldet');

  // Nächste Set-Nummer bestimmen (höchste vorhandene + 1)
  const sessionDir = path.join(calibDataDir, sessionName);
  const existing = await fsPromises.readdir(sessionDir);
  const maxSet = existing.reduce((max, f) => {
    const m = f.match(/_(\d+)\.jpg$/);
    return m ? Math.max(max, parseInt(m[1], 10)) : max;
  }, 0);
  const setNr = String(maxSet + 1).padStart(3, '0');

  const savedFiles = [];
  for (const camId of cameraIds) {
    let response;
    // Bei 503 (Kamera kurz busy nach Hires-Grab) einmal nach 2 s neu versuchen
    for (let attempt = 1; attempt <= 2; attempt++) {
      response = await new WebcamClient(WEBCAM_URL).getSnapshot(camId, true);
      if (response.status !== 503) break;
      if (attempt < 2) await new Promise(r => setTimeout(r, 2000));
    }
    if (!response.ok) throw new Error(`getSnapshot(${camId}): HTTP ${response.status}`);
    const buffer = Buffer.from(await response.arrayBuffer());
    const filename = `${camId}_${setNr}.jpg`;
    await fsPromises.writeFile(path.join(sessionDir, filename), buffer);
    savedFiles.push(filename);
  }

  return { cameraIds, savedFiles, setNr: parseInt(setNr, 10) };
}

/** GET /api/calibration/current — aktuelle Session-Info */
app.get('/api/calibration/current', async (req, res) => {
  try {
    const session = await findLatestCalibSession();
    if (!session) return res.json({ session: null, meta: null });
    const meta = await readCalibMeta(session);
    return res.json({ session, meta });
  } catch (err) {
    return res.status(500).json({ error: String(err) });
  }
});

/** POST /api/calibration/new — neue Session anlegen + erste Fotos */
app.post('/api/calibration/new', async (req, res) => {
  try {
    const ts = makeTimestamp();
    const sessionDir = path.join(calibDataDir, ts);
    await fsPromises.mkdir(sessionDir, { recursive: true });

    const meta = { timestamp: ts, createdAt: new Date().toISOString(), cameras: [], imageSets: 0, imageCount: 0 };
    await writeCalibMeta(ts, meta);

    try {
      const capture = await capturePhotos(ts);
      meta.cameras    = capture.cameraIds;
      meta.imageSets  = capture.setNr;
      meta.imageCount = capture.setNr * capture.cameraIds.length;
      await writeCalibMeta(ts, meta);
      return res.json({ session: ts, meta, savedFiles: capture.savedFiles });
    } catch (captureErr) {
      // Session angelegt, aber Fotos nicht verfügbar → trotzdem OK zurück
      return res.json({ session: ts, meta, warning: String(captureErr) });
    }
  } catch (err) {
    return res.status(500).json({ error: String(err) });
  }
});

/** POST /api/calibration/foto — weitere Fotos für aktuelle Session */
app.post('/api/calibration/foto', async (req, res) => {
  try {
    const session = await findLatestCalibSession();
    if (!session) {
      return res.status(400).json({ error: 'Keine Session vorhanden. Bitte zuerst "Neue Kalibrierung anlegen".' });
    }

    const capture = await capturePhotos(session);

    const meta = await readCalibMeta(session) ?? {
      timestamp: session, createdAt: new Date().toISOString(),
      cameras: [], imageSets: 0, imageCount: 0
    };
    meta.cameras    = capture.cameraIds;
    meta.imageSets  = capture.setNr;
    meta.imageCount = capture.setNr * capture.cameraIds.length;
    await writeCalibMeta(session, meta);

    return res.json({ session, meta, savedFiles: capture.savedFiles });
  } catch (err) {
    return res.status(500).json({ error: String(err) });
  }
});

/**
 * POST /api/calibration/compute
 * Führt scripts/callibriate.py für eine Kamera aus.
 * Body: { camera: "cam0" }
 * Antwortet als Server-Sent Events (SSE): jede Zeile stdout/stderr als
 *   data: {"type":"log","text":"..."}
 * Abschluss:
 *   data: {"type":"done","exitCode":0}
 */
const PYTHON_BIN     = process.env.PYTHON_BIN     || 'python3';
const calibScriptPath = path.join(__dirname, '..', 'scripts', 'callibriate.py');

app.post('/api/calibration/compute', async (req, res) => {
  try {
    const { camera } = req.body ?? {};
    if (!camera) return res.status(400).json({ error: '"camera" parameter fehlt' });

    const session = await findLatestCalibSession();
    if (!session) return res.status(400).json({ error: 'Keine Kalibrierungs-Session vorhanden' });

    const sessionDir = path.join(calibDataDir, session);

    // SSE-Header – erst NACH den Validierungen senden
    res.setHeader('Content-Type',  'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection',    'keep-alive');
    res.flushHeaders();

    // Schreibt nur wenn die Verbindung noch offen ist
    const send = (obj) => {
      if (!res.writableEnded) res.write(`data: ${JSON.stringify(obj)}\n\n`);
    };

    send({ type: 'log', text: `▶ Session:    ${session}` });
    send({ type: 'log', text: `▶ Kamera:     ${camera}` });
    send({ type: 'log', text: `▶ Script:     ${calibScriptPath}` });
    send({ type: 'log', text: '' });

    const exitCode = await runScript([
      calibScriptPath,
      '--camera',     camera,
      '--input-dir',  sessionDir,
      '--output-dir', sessionDir,
    ], send);

    send({ type: 'done', exitCode });
    if (!res.writableEnded) res.end();

  } catch (err) {
    // Fehler VOR flushHeaders → normaler JSON-Fehler
    // Fehler NACH flushHeaders → SSE-Fehlerevent + close
    console.error('calibration/compute error:', err);
    if (!res.headersSent) {
      res.status(500).json({ error: String(err) });
    } else {
      try {
        res.write(`data: ${JSON.stringify({ type: 'log', text: `Server-Fehler: ${err.message}` })}\n\n`);
        res.write(`data: ${JSON.stringify({ type: 'done', exitCode: -1 })}\n\n`);
        res.end();
      } catch { /* Verbindung bereits geschlossen */ }
    }
  }
});

// ── Board-Erkennung ───────────────────────────────────────────────────────────

const boardDataDir  = path.join(__dirname, '..', 'data', 'board');
const homingDataDir = path.join(__dirname, '..', 'data', 'homing');
const ROBOT_JSON    = process.env.ROBOT_JSON
  || path.join(__dirname, '..', 'scripts', 'robot_1781069752019.json');
const SCRIPT_1      = path.join(__dirname, '..', 'scripts', '1_detect_aruco_observations.py');
const SCRIPT_2      = path.join(__dirname, '..', 'scripts', '2_estimate_camera_from_observations.py');
const SCRIPT_3B     = path.join(__dirname, '..', 'scripts', '3b_corner_marker_poses.py');
const SCRIPT_4B     = path.join(__dirname, '..', 'scripts', '4b_revolute_angle.py');

/**
 * Führt ein Python-Script aus und leitet stdout/stderr zeilenweise an `send` weiter.
 * Gibt den Exit-Code zurück (Promise<number>).
 */
function runScript(args, send) {
  return new Promise((resolve) => {
    const cmd = [PYTHON_BIN, '-u', ...args].join(' ');
    console.log(`[runScript] ${cmd}`);
    const proc = spawn(PYTHON_BIN, ['-u', ...args]);

    let outBuf = '';
    proc.stdout.on('data', chunk => {
      outBuf += chunk.toString();
      const lines = outBuf.split('\n');
      outBuf = lines.pop();
      for (const line of lines) send({ type: 'log', text: line });
    });

    let errBuf = '';
    proc.stderr.on('data', chunk => {
      errBuf += chunk.toString();
      const lines = errBuf.split('\n');
      errBuf = lines.pop();
      for (const line of lines) send({ type: 'log', text: `[stderr] ${line}` });
    });

    proc.on('error', err => {
      send({ type: 'log', text: `Fehler beim Starten: ${err.message}` });
      resolve(-1);
    });

    proc.on('close', code => {
      if (outBuf) send({ type: 'log', text: outBuf });
      if (errBuf) send({ type: 'log', text: `[stderr] ${errBuf}` });
      resolve(code ?? -1);
    });
  });
}

/**
 * Board-Pipeline: Snapshot + Script 1 + Script 2 (pro Kamera) + Script 3b.
 * Schreibt Ergebnisse nach runDir (muss bereits existieren).
 * Wird von /api/board/run UND /api/homing/run genutzt.
 *
 * @param {string}   runDir  – Zielverzeichnis (bereits erstellt)
 * @param {Function} send    – SSE-Send-Funktion (obj => void)
 * @param {{ refSet?: string }} [opts]
 */
async function runBoardPipeline(runDir, send, { refSet } = {}) {
  // Kameras ermitteln
  if (!WEBCAM_URL) throw new Error('WEBCAM_URL nicht konfiguriert');
  const camData   = await new WebcamClient(WEBCAM_URL).getCameras();
  const cameraIds = (camData.cameras ?? []).map(c => c.id);
  send({ type: 'log', text: `▶ Kameras: ${cameraIds.join(', ')}` });
  send({ type: 'log', text: '' });

  // Pro Kamera: Foto → Script 1 → Script 2
  for (const camId of cameraIds) {
    send({ type: 'log', text: `─── ${camId} ${'─'.repeat(40 - camId.length)}` });

    // Snapshot
    send({ type: 'log', text: 'Foto aufnehmen …' });
    let snapResp;
    for (let attempt = 1; attempt <= 2; attempt++) {
      snapResp = await new WebcamClient(WEBCAM_URL).getSnapshot(camId, true);
      if (snapResp.status !== 503) break;
      if (attempt < 2) await new Promise(r => setTimeout(r, 2000));
    }
    if (!snapResp.ok) {
      send({ type: 'log', text: `⚠ HTTP ${snapResp.status} – Kamera übersprungen` });
      continue;
    }
    const imgPath = path.join(runDir, `${camId}.jpg`);
    await fsPromises.writeFile(imgPath, Buffer.from(await snapResp.arrayBuffer()));
    send({ type: 'log', text: `✅ Foto: ${camId}.jpg` });

    // NPZ suchen – neueste Session, die eine NPZ für diese Kamera enthält
    const npzInfo = await findLatestNpzForCamera(camId);
    if (!npzInfo) {
      send({ type: 'log', text: `⚠ Keine NPZ für ${camId} gefunden – übersprungen` });
      continue;
    }
    const npzPath = npzInfo.npzPath;
    send({ type: 'log', text: `▶ NPZ: data/calibration/${npzInfo.session}/${camId}_calibration.npz` });

    // Script 1 – ArUco-Erkennung
    send({ type: 'log', text: '\n▷ 1_detect_aruco_observations' });
    const exit1 = await runScript([
      SCRIPT_1,
      '-i',          imgPath,
      '-npz',        npzPath,
      '-robot',      ROBOT_JSON,
      '-cameraId',   camId,
      '-outDir',     runDir,
      '--saveDebugImage',
    ], send);
    if (exit1 !== 0) {
      send({ type: 'log', text: `❌ Script 1 Exit ${exit1}` });
      continue;
    }

    // Script 2 – Kamera-Pose schätzen
    const detJson = path.join(runDir, `${camId}_aruco_detection.json`);
    try { await fsPromises.access(detJson); }
    catch {
      send({ type: 'log', text: '⚠ Detection-JSON fehlt – Script 2 übersprungen' });
      continue;
    }
    send({ type: 'log', text: '\n▷ 2_estimate_camera_from_observations' });
    const script2Args = [SCRIPT_2, '-i', detJson, '-robot', ROBOT_JSON, '-outDir', runDir];
    if (refSet) script2Args.push('--refSet', refSet);
    const exit2 = await runScript(script2Args, send);
    if (exit2 !== 0) send({ type: 'log', text: `❌ Script 2 Exit ${exit2}` });

    send({ type: 'log', text: '' });
  }

  // Script 3b: Marker-Triangulierung (benötigt ≥2 Kamera-Posen)
  send({ type: 'log', text: '' });
  send({ type: 'log', text: '─── 3b: Marker-Triangulierung ────────────────────────────' });
  const runFiles3b = await fsPromises.readdir(runDir);
  const numPoses   = runFiles3b.filter(f => f.endsWith('_camera_pose.json')).length;
  if (numPoses >= 2) {
    send({ type: 'log', text: `▷ 3b_corner_marker_poses (${numPoses} Kamera-Posen)` });
    const exit3b = await runScript([
      SCRIPT_3B,
      '--evalDir', runDir,
      '--robot',   ROBOT_JSON,
    ], send);
    if (exit3b !== 0) send({ type: 'log', text: `❌ Script 3b Exit ${exit3b}` });
  } else {
    send({ type: 'log', text: `⚠ Nur ${numPoses} Kamera-Pose(n) – Script 3b braucht ≥2 Kameras` });
  }
  send({ type: 'log', text: '' });
}

/**
 * POST /api/board/run
 * 1. Erstellt data/board/{timestamp}/
 * 2. Holt Snapshot jeder Kamera
 * 3. Für jede Kamera: Script 1 (ArUco-Erkennung) → Script 2 (Kamera-Pose)
 * SSE-Stream während der Ausführung.
 */
app.post('/api/board/run', async (req, res) => {
  try {
    res.setHeader('Content-Type',  'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection',    'keep-alive');
    res.flushHeaders();

    const send = (obj) => {
      if (!res.writableEnded) res.write(`data: ${JSON.stringify(obj)}\n\n`);
    };

    const { refSet } = req.body ?? {};

    // 1. Temp-Verzeichnis
    const ts     = makeTimestamp();
    const runDir = path.join(boardDataDir, ts);
    await fsPromises.mkdir(runDir, { recursive: true });
    send({ type: 'log', text: `▶ Board-Run: ${ts}` });
    send({ type: 'log', text: `▶ Ordner:    ${runDir}` });

    // Robot-JSON laden und Marker-Anzahl loggen
    let robotData = null;
    try { robotData = JSON.parse(await fsPromises.readFile(ROBOT_JSON, 'utf8')); } catch {}
    const boardMarkers  = robotData?.links?.Board?.markers ?? [];
    const boardMarkerCount = boardMarkers.length;
    const refMarkerCount   = refSet
      ? boardMarkers.filter(m => m.set === refSet).length
      : boardMarkerCount;
    send({ type: 'log', text: `▶ Robot-JSON: ${ROBOT_JSON}` });
    send({ type: 'log', text: `▶ Board-Marker: ${boardMarkerCount} (links.Board.markers)` });
    send({ type: 'log', text: `▶ Referenz-Set: ${refSet ? `"${refSet}" (${refMarkerCount} Marker)` : 'alle'}` });
    send({ type: 'log', text: '' });

    // 2–3b: Board-Pipeline (Foto + Scripts 1, 2, 3b)
    await runBoardPipeline(runDir, send, { refSet });

    send({ type: 'log',  text: `✅ Board-Run abgeschlossen: ${ts}` });
    send({ type: 'done', exitCode: 0, runDir: ts });
    if (!res.writableEnded) res.end();

  } catch (err) {
    console.error('board/run error:', err);
    if (!res.headersSent) {
      res.status(500).json({ error: String(err) });
    } else {
      try {
        res.write(`data: ${JSON.stringify({ type: 'log',  text: `❌ ${err.message}` })}\n\n`);
        res.write(`data: ${JSON.stringify({ type: 'done', exitCode: -1 })}\n\n`);
        res.end();
      } catch {}
    }
  }
});

/** Alle Board-Run-Verzeichnisse, neueste zuerst */
async function listBoardRuns() {
  try {
    await fsPromises.access(boardDataDir);
    const entries = await fsPromises.readdir(boardDataDir, { withFileTypes: true });
    return entries.filter(e => e.isDirectory()).map(e => e.name).sort().reverse();
  } catch {
    return [];
  }
}

/** Neuestes Board-Run-Verzeichnis (Timestamp-Name) oder null */
async function findLatestBoardRun() {
  const dirs = await listBoardRuns();
  return dirs[0] ?? null;
}

/**
 * GET /api/board/runs?limit=N
 * Gibt eine Liste der vorhandenen Board-Run-Verzeichnisse zurück (neueste zuerst).
 */
app.get('/api/board/runs', async (req, res) => {
  try {
    const limit = Math.max(1, Math.min(50, parseInt(req.query.limit ?? '10', 10)));
    const runs = await listBoardRuns();
    return res.json({ runs: runs.slice(0, limit) });
  } catch (err) {
    return res.status(500).json({ error: String(err) });
  }
});

/**
 * GET /api/board/latest?run=<timestamp>&from=homing
 * Gibt Daten eines Board-Runs zurück: robot.json + Detection-Ergebnisse + Kamera-Posen.
 * Ohne ?run → neuester Run. Mit ?run=<timestamp> → genau dieser Run.
 * ?from=homing → liest aus data/homing/ statt data/board/ (für boardViewer im Homing-Mode).
 * Wird vom Board-Viewer (boardViewer.html) abgefragt.
 */
app.get('/api/board/latest', async (req, res) => {
  try {
    const fromHoming = req.query.from === 'homing';
    const dataDir    = fromHoming ? homingDataDir : boardDataDir;

    let runName = req.query.run;
    if (!runName) {
      if (fromHoming) {
        try {
          const entries = await fsPromises.readdir(dataDir, { withFileTypes: true });
          runName = entries.filter(e => e.isDirectory()).map(e => e.name).sort().reverse()[0] ?? null;
        } catch { runName = null; }
      } else {
        runName = await findLatestBoardRun();
      }
    }
    if (!runName) return res.json({ runDir: null, robot: null, detections: [], cameraPoses: [] });

    const runDir = path.join(dataDir, runName);

    let robot = null;
    try { robot = JSON.parse(await fsPromises.readFile(ROBOT_JSON, 'utf8')); } catch {}

    let files = [];
    try { files = await fsPromises.readdir(runDir); } catch {}

    const detections  = [];
    const cameraPoses = [];

    for (const f of files.sort()) {
      if (f.endsWith('_aruco_detection.json')) {
        try {
          const data = JSON.parse(await fsPromises.readFile(path.join(runDir, f), 'utf8'));
          detections.push({
            file:              f,
            cameraId:          data.camera?.camera_id ?? f.replace('_aruco_detection.json', ''),
            detectedMarkerIds: (data.detections ?? []).map(d => d.marker_id),
            numDetected:       data.aruco?.num_detected_markers ?? 0,
            numRejected:       data.aruco?.num_rejected_candidates ?? 0,
          });
        } catch {}
      } else if (f.endsWith('_camera_pose.json')) {
        try {
          const data = JSON.parse(await fsPromises.readFile(path.join(runDir, f), 'utf8'));
          const cp = data.camera_pose;
          cameraPoses.push({
            file:            f,
            cameraId:        data.camera?.camera_id ?? f.replace('_camera_pose.json', ''),
            position_mm:     cp?.camera_in_world?.position_mm     ?? null,
            rotation_matrix: cp?.world_to_camera?.rotation_matrix ?? null,
            usedMarkerIds:   data.estimation?.used_marker_ids     ?? [],
            rms_px:          data.estimation?.residual_rms_px     ?? null,
          });
        } catch {}
      }
    }

    // aruco_marker_poses.json (Ausgabe von 3b_corner_marker_poses.py)
    let measuredMarkers = null;
    try {
      const raw = await fsPromises.readFile(path.join(runDir, 'aruco_marker_poses.json'), 'utf8');
      measuredMarkers = JSON.parse(raw);
    } catch {}

    return res.json({ runDir: runName, robotFile: path.basename(ROBOT_JSON), robot, detections, cameraPoses, measuredMarkers });
  } catch (err) {
    return res.status(500).json({ error: String(err) });
  }
});

// ── Homing ───────────────────────────────────────────────────────────────────

/**
 * POST /api/homing/run
 * Vollständiger Homing-Ablauf: Board-Pipeline + 4b-Kette (SSE-Stream).
 */
app.post('/api/homing/run', async (req, res) => {
  res.setHeader('Content-Type',  'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection',    'keep-alive');
  res.flushHeaders();

  const send = (obj) => {
    if (!res.writableEnded) res.write(`data: ${JSON.stringify(obj)}\n\n`);
  };

  try {
    await fsPromises.mkdir(homingDataDir, { recursive: true });
    await runHoming({
      robotJsonPath:    ROBOT_JSON,
      homingDir:        homingDataDir,
      send,
      runScript,
      runBoardPipeline,
      SCRIPT_4B,
    });
  } catch (err) {
    console.error('homing/run error:', err);
    try {
      send({ type: 'error', text: String(err) });
      send({ type: 'done', exitCode: -1 });
    } catch {}
  }
  if (!res.writableEnded) res.end();
});

/**
 * POST /api/homing/send-state
 * Sendet { state: { x, y, z, a, b, c, e } } an ROBOT_URL/api/state.
 */
app.post('/api/homing/send-state', async (req, res) => {
  try {
    const { state } = req.body ?? {};
    if (!state) return res.status(400).json({ error: '"state" fehlt' });
    if (!ROBOT_URL) return res.status(501).json({ error: 'ROBOT_URL ist nicht konfiguriert' });

    const url = new URL('/api/state', ROBOT_URL).toString();
    const upstream = await fetch(url, {
      method:  'POST',
      headers: { 'Content-Type': 'application/json' },
      body:    JSON.stringify(state),
    });
    if (!upstream.ok) {
      const text = await upstream.text();
      return res.status(upstream.status).json({ error: `Robot-Fehler: ${text}` });
    }
    const result = await upstream.json().catch(() => ({}));
    return res.json({ ok: true, result });
  } catch (err) {
    console.error('homing/send-state error:', err);
    return res.status(500).json({ error: String(err) });
  }
});

/**
 * GET /api/homing/run-data?run=<timestamp>
 * Gibt Bilder (base64) und JSON-Dateien eines Homing-Runs zurück.
 */
app.get('/api/homing/run-data', async (req, res) => {
  try {
    const runName = req.query.run;
    if (!runName) return res.status(400).json({ error: '"run" parameter fehlt' });
    const runDir = path.join(homingDataDir, runName);
    let files = [];
    try { files = await fsPromises.readdir(runDir); } catch {}

    const images = [];
    for (const f of files.sort()) {
      if (/\.(jpg|jpeg|png)$/i.test(f)) {
        try {
          const buf = await fsPromises.readFile(path.join(runDir, f));
          images.push({ filename: f, contentBase64: buf.toString('base64'), mimeType: 'image/jpeg' });
        } catch {}
      }
    }

    // Letzten accumulated_state zurückgeben
    let finalState = null;
    const stateFiles = files.filter(f => f.startsWith('state_') && f.endsWith('.json')).sort();
    if (stateFiles.length > 0) {
      try {
        const raw = await fsPromises.readFile(path.join(runDir, stateFiles[stateFiles.length - 1]), 'utf8');
        finalState = JSON.parse(raw).accumulated_state ?? null;
      } catch {}
    }

    // aruco_marker_poses.csv für Snapshot-CSV-Tabelle
    let csvContent = null;
    try {
      csvContent = await fsPromises.readFile(path.join(runDir, 'aruco_marker_poses.csv'), 'utf8');
    } catch {}

    return res.json({ runDir: runName, images, finalState, csvContent });
  } catch (err) {
    return res.status(500).json({ error: String(err) });
  }
});

// ── Robot-JSON bearbeiten ─────────────────────────────────────────────────────

/**
 * POST /api/robot/assign-by-z
 * Weist allen Markern in [zMin, zMax] mm das angegebene Set und/oder Link zu.
 * Body: { zMin, zMax, set?, link? }
 */
app.post('/api/robot/assign-by-z', async (req, res) => {
  try {
    const { zMin, zMax, set, link } = req.body ?? {};
    if (zMin == null || zMax == null) {
      return res.status(400).json({ error: 'zMin und zMax sind erforderlich' });
    }
    if (!set && !link) {
      return res.status(400).json({ error: 'Mindestens set oder link muss angegeben werden' });
    }

    // Triangulierte Marker aus dem letzten Board-Run als Zusatzquelle für
    // Marker, die noch nicht in robot.json stehen (z.B. neu entdeckte Marker)
    let extraMarkers = [];
    try {
      const latestRun = await findLatestBoardRun();
      if (latestRun) {
        const posesPath = path.join(boardDataDir, latestRun, 'aruco_marker_poses.json');
        const poses = JSON.parse(await fsPromises.readFile(posesPath, 'utf8'));
        extraMarkers = poses.markers ?? [];
      }
    } catch { /* kein 3b-Output vorhanden – nur bestehende robot.json-Marker bearbeiten */ }

    const result = await assignByZRange(ROBOT_JSON, { zMin, zMax, set, link, extraMarkers });
    const added   = result.changes.filter(c => c.action === 'added').length;
    const updated = result.changes.filter(c => c.action === 'updated').length;
    console.log(`robot/assign-by-z z=[${zMin}..${zMax}] set="${set}" link="${link}" → ${updated} aktualisiert, ${added} neu (von ${extraMarkers.length} 3b-Markern)`);
    return res.json(result);
  } catch (err) {
    console.error('robot/assign-by-z error:', err);
    return res.status(500).json({ error: String(err) });
  }
});

/**
 * POST /api/robot/remove-marker
 * Entfernt Set oder Link-Zuordnung eines Markers.
 * Body: { markerId, removeFrom }   removeFrom: 'set' | 'link'
 */
app.post('/api/robot/remove-marker', async (req, res) => {
  try {
    const { markerId, removeFrom } = req.body ?? {};
    if (markerId == null) {
      return res.status(400).json({ error: 'markerId ist erforderlich' });
    }
    if (!['set', 'link'].includes(removeFrom)) {
      return res.status(400).json({ error: 'removeFrom muss "set" oder "link" sein' });
    }
    const result = await removeMarkerAssignment(ROBOT_JSON, { markerId, removeFrom });
    console.log(`robot/remove-marker id=${markerId} from=${removeFrom} → changed=${result.changed}`);
    return res.json(result);
  } catch (err) {
    console.error('robot/remove-marker error:', err);
    return res.status(500).json({ error: String(err) });
  }
});

/**
 * GET /api/robot/board-sets
 * Gibt die einzigartigen "set"-Werte aller Marker in links.Board zurück.
 * Wird vom Frontend genutzt, um Dropdowns zu befüllen.
 */
app.get('/api/robot/board-sets', async (req, res) => {
  try {
    const robot = JSON.parse(await fsPromises.readFile(ROBOT_JSON, 'utf8'));
    const markers = robot?.links?.Board?.markers ?? [];
    const sets = [...new Set(markers.map(m => m.set).filter(Boolean))].sort();
    return res.json({ sets });
  } catch (err) {
    console.error('robot/board-sets error:', err);
    return res.status(500).json({ error: String(err) });
  }
});

/**
 * POST /api/robot/align-sets
 * Richtet alle Marker des angegebenen Sets rigid (2D-Rotation um Z + 3D-Translation)
 * an den aktuellen 3b-Messpositionen aus.
 * Body: { setToMove }
 */
app.post('/api/robot/align-sets', async (req, res) => {
  try {
    const { setToMove, setFixed } = req.body ?? {};
    if (!setToMove) return res.status(400).json({ error: '"setToMove" ist erforderlich.' });

    let extraMarkers = [];
    try {
      const latestRun = await findLatestBoardRun();
      if (latestRun) {
        const posesPath = path.join(boardDataDir, latestRun, 'aruco_marker_poses.json');
        const poses = JSON.parse(await fsPromises.readFile(posesPath, 'utf8'));
        extraMarkers = poses.markers ?? [];
      }
    } catch { /* kein 3b-Output vorhanden */ }

    const result = await alignSetToMeasured(ROBOT_JSON, { setToMove, extraMarkers });
    if (result.error) return res.status(400).json(result);

    console.log(
      `robot/align-sets fixed="${setFixed ?? '–'}" move="${setToMove}" → ${result.numChanged} Marker` +
      ` (${result.numMatchingPts} Messpunkte) Δx=${result.transform.tx} Δy=${result.transform.ty}` +
      ` Δz=${result.transform.tz} mm θ=${result.transform.thetaDeg}°`,
    );
    return res.json(result);
  } catch (err) {
    console.error('robot/align-sets error:', err);
    return res.status(500).json({ error: String(err) });
  }
});

/**
 * POST /api/robot/assign-id
 * Fügt einen einzelnen Marker per ID zu Set und Link hinzu oder aktualisiert ihn.
 * Body: { markerId, set?, link? }
 */
app.post('/api/robot/assign-id', async (req, res) => {
  try {
    const { markerId, set, link } = req.body ?? {};
    if (markerId == null) return res.status(400).json({ error: '"markerId" ist erforderlich.' });

    let extraMarkers = [];
    try {
      const latestRun = await findLatestBoardRun();
      if (latestRun) {
        const posesPath = path.join(boardDataDir, latestRun, 'aruco_marker_poses.json');
        const poses = JSON.parse(await fsPromises.readFile(posesPath, 'utf8'));
        extraMarkers = poses.markers ?? [];
      }
    } catch { /* kein 3b-Output vorhanden */ }

    const result = await assignMarkerId(ROBOT_JSON, { markerId, set, link, extraMarkers });
    if (!result.changed && result.error) return res.status(400).json(result);

    console.log(
      `robot/assign-id id=${markerId} set="${set ?? ''}" link="${link ?? ''}"` +
      ` → ${result.change?.action ?? 'unverändert'}`,
    );
    return res.json(result);
  } catch (err) {
    console.error('robot/assign-id error:', err);
    return res.status(500).json({ error: String(err) });
  }
});

/**
 * POST /api/robot/adopt-x-axis
 * Dreht alle Marker-Positionen in robot.json so, dass die gemessene Richtung
 * zur neuen X-Achse [1,0,0] wird.  Rotation um den A0-Schwerpunkt.
 * Body: { direction: [vx, vy, vz] }
 */
app.post('/api/robot/adopt-x-axis', async (req, res) => {
  try {
    const { direction } = req.body ?? {};
    if (!Array.isArray(direction) || direction.length < 3) {
      return res.status(400).json({ error: '"direction" muss ein Array [vx,vy,vz] sein.' });
    }
    const result = await adoptXAxis(ROBOT_JSON, { direction });
    console.log(
      `robot/adopt-x-axis dir=[${direction.map(v => Number(v).toFixed(4)).join(', ')}]` +
      ` → ${result.numChanged} Marker, Ursprung=[${result.origin.join(', ')}]` +
      ` XY=${result.angleXYdeg}° XZ=${result.angleXZdeg}°`,
    );
    return res.json(result);
  } catch (err) {
    console.error('robot/adopt-x-axis error:', err);
    return res.status(500).json({ error: String(err) });
  }
});

/**
 * POST /api/robot/assign-fixed-markers
 * Ordnet Marker, die sich bei einer Gelenk-Rotation kaum bewegen, dem
 * angegebenen Link zu (typisch: 'Base').
 * Body: { markerIds: number[], targetLink: string, measuredPositions: [{id, position_mm}] }
 */
app.post('/api/robot/assign-fixed-markers', async (req, res) => {
  try {
    const { markerIds, targetLink, measuredPositions = [] } = req.body ?? {};
    if (!Array.isArray(markerIds) || markerIds.length === 0) {
      return res.status(400).json({ error: '"markerIds" muss ein nicht-leeres Array sein.' });
    }
    if (!targetLink) {
      return res.status(400).json({ error: '"targetLink" muss angegeben werden.' });
    }
    const result = await assignFixedMarkersToLink(ROBOT_JSON, { markerIds, targetLink, measuredPositions });
    console.log(
      `robot/assign-fixed-markers [${markerIds.join(',')}] → ${targetLink}` +
      `  added=${result.numAdded}  alreadyPresent=${result.numAlreadyPresent}`,
    );
    return res.json(result);
  } catch (err) {
    console.error('robot/assign-fixed-markers error:', err);
    return res.status(500).json({ error: String(err) });
  }
});

/**
 * POST /api/robot/set-joint-origin
 * Setzt Y und Z des jointToParent.origin eines Links aus der berechneten
 * Drehachsen-Position.
 * Body: { linkName: string, y: number, z: number }
 */
app.post('/api/robot/set-joint-origin', async (req, res) => {
  try {
    const { linkName, y, z } = req.body ?? {};
    if (!linkName) return res.status(400).json({ error: '"linkName" muss angegeben werden.' });
    if (!Number.isFinite(Number(y)) || !Number.isFinite(Number(z))) {
      return res.status(400).json({ error: '"y" und "z" müssen Zahlen sein.' });
    }
    const result = await setJointOriginYZ(ROBOT_JSON, { linkName, y: Number(y), z: Number(z) });
    if (!result.changed) {
      return res.status(400).json({ error: result.error });
    }
    console.log(
      `robot/set-joint-origin ${linkName}: ` +
      `[${result.oldOrigin.join(', ')}] → [${result.newOrigin.join(', ')}]`,
    );
    return res.json(result);
  } catch (err) {
    console.error('robot/set-joint-origin error:', err);
    return res.status(500).json({ error: String(err) });
  }
});

/**
 * POST /api/calibration/upload-npz
 * Liest {camera}_calibration.npz aus der aktuellen Session und
 * schickt sie per PUT an den Webcam-Service.
 * Body: { camera: "cam0" }
 */
app.post('/api/calibration/upload-npz', async (req, res) => {
  try {
    const { camera } = req.body ?? {};
    if (!camera) return res.status(400).json({ error: '"camera" parameter fehlt' });
    if (!WEBCAM_URL) return res.status(501).json({ error: 'WEBCAM_URL ist nicht konfiguriert' });

    const session = await findLatestCalibSession();
    if (!session) return res.status(400).json({ error: 'Keine Kalibrierungs-Session vorhanden' });

    const npzPath = path.join(calibDataDir, session, `${camera}_calibration.npz`);

    try {
      await fsPromises.access(npzPath);
    } catch {
      return res.status(404).json({
        error: `Datei nicht gefunden: ${camera}_calibration.npz — bitte zuerst "Kalibrierung berechnen".`
      });
    }

    const npzData = await fsPromises.readFile(npzPath);
    const putUrl  = new URL(`/api/cameras/${camera}/calibration`, WEBCAM_URL).toString();

    const putRes = await fetch(putUrl, {
      method:  'PUT',
      headers: { 'Content-Type': 'application/octet-stream' },
      body:    npzData,
    });

    if (!putRes.ok) {
      const text = await putRes.text();
      return res.status(putRes.status).json({ error: `Webcam-Service: ${putRes.status} – ${text}` });
    }

    const result = await putRes.json();
    return res.json({ ok: true, camera, session, size: npzData.length, webcam: result });

  } catch (err) {
    console.error('calibration/upload-npz error:', err);
    return res.status(500).json({ error: String(err) });
  }
});

// ── X-Achse / Rotations-Detektion ────────────────────────────────────────────

const xaxisDataDir = path.join(__dirname, '..', 'data', 'xaxis');
const ROTATION_DETECTION_FILE = path.join(xaxisDataDir, 'rotation_detection.json');

/** POST /api/xaxis/save-rotation-detection
 *  Speichert eine Achsmessung an rotation_detection.json (append-Modus). */
app.post('/api/xaxis/save-rotation-detection', express.json(), async (req, res) => {
  try {
    const { axis, runs, numMarkers, markers } = req.body ?? {};
    if (!axis || !axis.dir || !axis.referencePoint) {
      return res.status(400).json({ error: 'Ungültige Nutzlast: axis.dir und axis.referencePoint erwartet' });
    }

    // Verzeichnis anlegen falls nötig
    await fsPromises.mkdir(xaxisDataDir, { recursive: true });

    // Bestehende Einträge lesen oder leer beginnen
    let entries = [];
    try {
      const raw = await fsPromises.readFile(ROTATION_DETECTION_FILE, 'utf-8');
      entries = JSON.parse(raw);
      if (!Array.isArray(entries)) entries = [];
    } catch {
      // Datei existiert noch nicht – kein Fehler
    }

    const newEntry = {
      timestamp: new Date().toISOString(),
      runs: runs ?? {},
      axis,
      numMarkers: numMarkers ?? null,
      markers: markers ?? [],
    };

    entries.push(newEntry);
    await fsPromises.writeFile(ROTATION_DETECTION_FILE, JSON.stringify(entries, null, 2), 'utf-8');

    return res.json({
      file: 'data/xaxis/rotation_detection.json',
      total: entries.length,
    });
  } catch (err) {
    console.error('save-rotation-detection error:', err);
    return res.status(500).json({ error: String(err) });
  }
});

async function checkServiceReachability(name, url) {
  try {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 5000);
    const res = await fetch(url, { signal: controller.signal });
    clearTimeout(timeout);

    if (!res.ok) {
      console.warn(`${name} ist nicht vollständig erreichbar (${res.status}) unter ${url}`);
      return false;
    }

    console.log(`${name} erreichbar unter ${url}`);
    return true;
  } catch (err) {
    console.warn(`${name} konnte nicht erreicht werden unter ${url}:`, err.message || err);
    return false;
  }
}

async function createHttpsServer() {
  try {
    await fsPromises.access(HTTPS_KEY_PATH);
    await fsPromises.access(HTTPS_CERT_PATH);

    const key = fs.readFileSync(HTTPS_KEY_PATH);
    const cert = fs.readFileSync(HTTPS_CERT_PATH);
    const httpsOptions = { key, cert, passphrase: HTTPS_PASSPHRASE };

    console.log(`HTTPS-Zertifikate geladen: ${HTTPS_KEY_PATH}, ${HTTPS_CERT_PATH}`);
    return https.createServer(httpsOptions, app);
  } catch (err) {
    console.warn('HTTPS-Zertifikate konnten nicht geladen werden:', err.message || err);
    console.warn('Fallback auf HTTP. External proxy muss HTTPS terminieren.');
    return null;
  }
}

async function startServer() {
  if (WEBCAM_URL) {
    await checkServiceReachability('WEBCAM_URL', new URL('/health', WEBCAM_URL).toString());
  }

  if (BODYTRACKER_URL) {
    await checkServiceReachability('BODYTRACKER_URL', new URL('/v1/health', BODYTRACKER_URL).toString());
  }

  const server = await createHttpsServer();
  const isHttps = Boolean(server);
  const listenServer = server || app;

  listenServer.listen(PORT, () => {
    console.log(`appRobotHoming backend listening on port ${PORT} (${isHttps ? 'HTTPS' : 'HTTP'})`);
    console.log(`WEBCAM_URL=${WEBCAM_URL || '<lokal>'}`);
    console.log(`BODYTRACKER_URL=${BODYTRACKER_URL || '<nicht konfiguriert>'}`);
  });
}

startServer().catch((err) => {
  console.error('Fehler beim Starten des Servers:', err);
  console.log('Starte trotzdem den Server weiter...');
  app.listen(PORT, () => {
    console.log(`appRobotHoming backend listening on port ${PORT} (HTTP)`);
    console.log(`WEBCAM_URL=${WEBCAM_URL || '<lokal>'}`);
    console.log(`BODYTRACKER_URL=${BODYTRACKER_URL || '<nicht konfiguriert>'}`);
  });
});
