free-coding-models 0.3.40 → 0.3.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/web/server.js CHANGED
@@ -11,6 +11,7 @@
11
11
  * GET /styles.css → Dashboard styles
12
12
  * GET /app.js → Dashboard client JS
13
13
  * GET /api/models → All model metadata (JSON)
14
+ * GET /api/health → Lightweight dashboard health probe
14
15
  * GET /api/config → Current config (sanitized — masked keys)
15
16
  * GET /api/key/:prov → Reveal a provider's full API key
16
17
  * GET /api/events → SSE stream of live ping results
@@ -32,6 +33,7 @@ import {
32
33
  } from '../src/utils.js'
33
34
 
34
35
  const __dirname = dirname(fileURLToPath(import.meta.url))
36
+ const SERVER_SIGNATURE = 'free-coding-models-web'
35
37
 
36
38
  // ─── State ───────────────────────────────────────────────────────────────────
37
39
 
@@ -215,6 +217,8 @@ function serveDistFile(res, pathname) {
215
217
  }
216
218
 
217
219
  function handleRequest(req, res) {
220
+ res.setHeader('X-FCM-Server', SERVER_SIGNATURE)
221
+
218
222
  // CORS for local dev
219
223
  res.setHeader('Access-Control-Allow-Origin', '*')
220
224
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
@@ -265,6 +269,11 @@ function handleRequest(req, res) {
265
269
  res.end(JSON.stringify(getModelsPayload()))
266
270
  break
267
271
 
272
+ case '/api/health':
273
+ res.writeHead(200, { 'Content-Type': 'application/json' })
274
+ res.end(JSON.stringify({ ok: true, app: SERVER_SIGNATURE }))
275
+ break
276
+
268
277
  case '/api/config':
269
278
  res.writeHead(200, { 'Content-Type': 'application/json' })
270
279
  res.end(JSON.stringify(getConfigPayload()))
@@ -335,6 +344,40 @@ function checkPortInUse(port) {
335
344
  })
336
345
  }
337
346
 
347
+ export async function inspectExistingWebServer(port) {
348
+ const inUse = await checkPortInUse(port)
349
+ if (!inUse) return { inUse: false, isFcm: false }
350
+
351
+ const controller = new AbortController()
352
+ const timeout = setTimeout(() => controller.abort(), 750)
353
+
354
+ try {
355
+ // 📖 Probe a tiny health route so we only reuse a port when the running
356
+ // 📖 process is actually the free-coding-models dashboard, not any random app.
357
+ const response = await fetch(`http://127.0.0.1:${port}/api/health`, {
358
+ signal: controller.signal,
359
+ headers: { Accept: 'application/json' },
360
+ })
361
+ const payload = await response.json().catch(() => null)
362
+ const signature = response.headers.get('x-fcm-server')
363
+ return {
364
+ inUse: true,
365
+ isFcm: signature === SERVER_SIGNATURE || payload?.app === SERVER_SIGNATURE,
366
+ }
367
+ } catch {
368
+ return { inUse: true, isFcm: false }
369
+ } finally {
370
+ clearTimeout(timeout)
371
+ }
372
+ }
373
+
374
+ export async function findAvailablePort(startPort, maxAttempts = 20) {
375
+ for (let port = startPort; port < startPort + maxAttempts; port++) {
376
+ if (!(await checkPortInUse(port))) return port
377
+ }
378
+ throw new Error(`No free local port found between ${startPort} and ${startPort + maxAttempts - 1}`)
379
+ }
380
+
338
381
  function openBrowser(url) {
339
382
  const cmd = process.platform === 'darwin' ? 'open'
340
383
  : process.platform === 'win32' ? 'start'
@@ -346,11 +389,12 @@ function openBrowser(url) {
346
389
 
347
390
  // ─── Exports ─────────────────────────────────────────────────────────────────
348
391
 
349
- export async function startWebServer(port = 3333, { open = true } = {}) {
350
- const alreadyRunning = await checkPortInUse(port)
351
- const url = `http://localhost:${port}`
392
+ export async function startWebServer(port = 3333, { open = true, startPingLoop = true } = {}) {
393
+ const portStatus = await inspectExistingWebServer(port)
394
+
395
+ if (portStatus.inUse && portStatus.isFcm) {
396
+ const url = `http://localhost:${port}`
352
397
 
353
- if (alreadyRunning) {
354
398
  console.log()
355
399
  console.log(` ⚡ free-coding-models Web Dashboard already running`)
356
400
  console.log(` 🌐 ${url}`)
@@ -359,9 +403,21 @@ export async function startWebServer(port = 3333, { open = true } = {}) {
359
403
  return null
360
404
  }
361
405
 
406
+ let resolvedPort = port
407
+ if (portStatus.inUse && !portStatus.isFcm) {
408
+ resolvedPort = await findAvailablePort(port + 1)
409
+ console.log()
410
+ console.log(` ⚠️ Port ${port} is already in use by another local app`)
411
+ console.log(` ↪ Starting free-coding-models Web Dashboard on port ${resolvedPort} instead`)
412
+ console.log()
413
+ }
414
+
415
+ const url = `http://localhost:${resolvedPort}`
416
+
362
417
  const server = createServer(handleRequest)
418
+ let pingLoopTimer = null
363
419
 
364
- server.listen(port, () => {
420
+ server.listen(resolvedPort, () => {
365
421
  console.log()
366
422
  console.log(` ⚡ free-coding-models Web Dashboard`)
367
423
  console.log(` 🌐 ${url}`)
@@ -373,10 +429,15 @@ export async function startWebServer(port = 3333, { open = true } = {}) {
373
429
  })
374
430
 
375
431
  async function schedulePingLoop() {
432
+ if (!server.listening) return
376
433
  await pingAllModels()
377
- setTimeout(schedulePingLoop, 10_000)
434
+ pingLoopTimer = setTimeout(schedulePingLoop, 10_000)
378
435
  }
379
- schedulePingLoop()
436
+
437
+ if (startPingLoop) schedulePingLoop()
438
+ server.on('close', () => {
439
+ if (pingLoopTimer) clearTimeout(pingLoopTimer)
440
+ })
380
441
 
381
442
  return server
382
443
  }