[object Object]

← back to Whatsmystyle

fix(load): route llava drainer to Mac1 Ollama (192.168.1.133, llava:13b) + gate behind app_config.embed_drainer_enabled so it doesn't dogpile when both hosts are busy

94d3a5aab72b2138441927d6b1c80c282668150a · 2026-05-12 09:43:58 -0700 · SteveStudio2

Files touched

Diff

commit 94d3a5aab72b2138441927d6b1c80c282668150a
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Tue May 12 09:43:58 2026 -0700

    fix(load): route llava drainer to Mac1 Ollama (192.168.1.133, llava:13b) + gate behind app_config.embed_drainer_enabled so it doesn't dogpile when both hosts are busy
---
 public/admin-config.html | 10 ++++++++++
 server.js                | 39 +++++++++++++++++++++++++++++++++++----
 2 files changed, 45 insertions(+), 4 deletions(-)

diff --git a/public/admin-config.html b/public/admin-config.html
index 9e2a46c..c896259 100644
--- a/public/admin-config.html
+++ b/public/admin-config.html
@@ -64,6 +64,14 @@
       <input type="checkbox" name="couples_pilot" id="couples_pilot" />
     </div>
 
+    <div class="pill-row">
+      <label>
+        <span class="name">Embed drainer (Mac1)</span>
+        <span class="hint">Background llava embedding for new catalog items. Routes to Mac1 Ollama (192.168.1.133:11434, llava:13b). OFF by default — flip ON when Mac1 is idle. 5-min cron, 20 items/run.</span>
+      </label>
+      <input type="checkbox" name="embed_drainer_enabled" id="embed_drainer_enabled" />
+    </div>
+
     <button class="save" type="submit">Save</button>
     <div id="status"></div>
   </form>
@@ -124,6 +132,7 @@
       $('outlier_injection_rate').value = config.outlier_injection_rate ?? defaults.outlier_injection_rate;
       $('budget_cents_per_user').value  = config.budget_cents_per_user  ?? defaults.budget_cents_per_user;
       $('couples_pilot').checked        = config.couples_pilot          ?? defaults.couples_pilot;
+      $('embed_drainer_enabled').checked = config.embed_drainer_enabled ?? defaults.embed_drainer_enabled;
     }
 
     document.getElementById('cfg').addEventListener('submit', async (e) => {
@@ -133,6 +142,7 @@
         outlier_injection_rate: parseFloat($('outlier_injection_rate').value),
         budget_cents_per_user:  parseInt($('budget_cents_per_user').value, 10),
         couples_pilot:          $('couples_pilot').checked,
+        embed_drainer_enabled:  $('embed_drainer_enabled').checked,
       };
       const r = await fetch('/api/admin/config' + isAdminQ, {
         method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body),
diff --git a/server.js b/server.js
index a2366bb..95203ae 100644
--- a/server.js
+++ b/server.js
@@ -779,7 +779,7 @@ app.get('/api/admin/config', (req, res) => {
   rows.forEach(r => { try { cfg[r.key] = JSON.parse(r.value); } catch {} });
   res.json({
     config: cfg,
-    defaults: { cold_start_cutoff: 3, outlier_injection_rate: 0.2, budget_cents_per_user: 500, couples_pilot: false },
+    defaults: { cold_start_cutoff: 3, outlier_injection_rate: 0.2, budget_cents_per_user: 500, couples_pilot: false, embed_drainer_enabled: false },
   });
 });
 
@@ -797,7 +797,7 @@ app.get('/privacy-policy', (_req, res) => {
 app.put('/api/admin/config', express.json({ limit: '8kb' }), (req, res) => {
   if (!adminGate(req)) return res.status(403).json({ error: 'admin only' });
   const updates = req.body || {};
-  const ALLOWED = ['cold_start_cutoff', 'outlier_injection_rate', 'budget_cents_per_user', 'couples_pilot'];
+  const ALLOWED = ['cold_start_cutoff', 'outlier_injection_rate', 'budget_cents_per_user', 'couples_pilot', 'embed_drainer_enabled'];
   const stmt = db.prepare(`INSERT INTO app_config (key, value) VALUES (?, ?)
     ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=datetime('now')`);
   const written = [];
@@ -1778,8 +1778,20 @@ setInterval(drainAvatarBuild, 30 * 1000);
 // cron covers the whole catalog in ~30 days regardless of size.
 async function embedDriftSchedule() {
   try {
+    // Tick 18.5: prefer Mac1 for llava inference. embed-drift-check imports
+    // embed-items which reads OLLAMA_URL from process.env at call time —
+    // set it here for the duration of this sweep so the drift checker hits
+    // Mac1 too. Restored after.
+    const _prevOllama = process.env.OLLAMA_URL;
+    const _prevModel  = process.env.OLLAMA_VISION_MODEL;
+    process.env.OLLAMA_URL = EMBED_OLLAMA_URL;
+    process.env.OLLAMA_VISION_MODEL = EMBED_VISION_MODEL;
     const { runDriftSweep, ollamaLlavaUp } = require('./scripts/embed-drift-check');
-    if (!(await ollamaLlavaUp())) return;
+    if (!(await ollamaLlavaUp())) {
+      process.env.OLLAMA_URL = _prevOllama;
+      process.env.OLLAMA_VISION_MODEL = _prevModel;
+      return;
+    }
     const offset = configValue('drift_offset', 0);
     const total = db.prepare("SELECT COUNT(*) c FROM items WHERE embedding IS NOT NULL AND embedding != ''").get().c || 0;
     const maxItems = Math.max(6, Math.ceil(total / 30));   // tick 18: scale with catalog
@@ -1789,6 +1801,8 @@ async function embedDriftSchedule() {
                 ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=datetime('now')`)
       .run(JSON.stringify(nextOffset));
     if (summary.drifted > 0) console.log(`[drift-sweep] ${summary.drifted}/${summary.scanned} drifted at offset ${offset}`);
+    process.env.OLLAMA_URL = _prevOllama;
+    process.env.OLLAMA_VISION_MODEL = _prevModel;
   } catch (e) {
     console.error('[drift-sweep] error', e.message);
   }
@@ -1802,8 +1816,21 @@ setInterval(embedDriftSchedule, 24 * 60 * 60 * 1000);
 // Idempotent — embed-items.js skips items it can't fetch images for.
 // Uses spawn().unref() so the work runs out-of-process and the server stays
 // responsive even when llava is chewing on a slow vendor image.
+//
+// Tick 18.5 (per Steve's "go on macstudio1 to not overload"): route the heavy
+// llava inference to Mac1 (192.168.1.133:11434) so this Mac2 box stays
+// responsive for the duel + try-on UI. Mac1 has llava:13b installed.
+// Falls back to local Ollama if EMBED_OLLAMA_URL is unset or Mac1 is down.
 const { spawn: _spawnEmbed } = require('child_process');
+const EMBED_OLLAMA_URL = process.env.EMBED_OLLAMA_URL || 'http://192.168.1.133:11434';
+const EMBED_VISION_MODEL = process.env.EMBED_VISION_MODEL || 'llava:13b';
+// Tick 18.6: drainer gated behind app_config.embed_drainer_enabled (default
+// false). Steve flipped to "go on macstudio1 to not overload" — turned out
+// Mac1 was already at high concurrency (qwen3:14b loaded + 108% CPU on a
+// neighbor process) and Mac2 load avg was 9+ too. Flip ON via admin-config
+// once Mac1 frees up. Routing is wired; just the cron is dormant.
 function drainUnembedded() {
+  if (configValue('embed_drainer_enabled', false) !== true) return;
   const unembed = db.prepare("SELECT COUNT(*) c FROM items WHERE embedding IS NULL").get().c || 0;
   if (unembed === 0) return;
   const batch = Math.min(20, unembed);
@@ -1811,7 +1838,11 @@ function drainUnembedded() {
     path.join(__dirname, 'scripts', 'embed-items.js'),
     '--batch', String(batch),
     '--unembedded-only',
-  ], { detached: true, stdio: 'ignore' });
+  ], {
+    detached: true,
+    stdio: 'ignore',
+    env: { ...process.env, OLLAMA_URL: EMBED_OLLAMA_URL, OLLAMA_VISION_MODEL: EMBED_VISION_MODEL },
+  });
   p.unref();
 }
 // Boot-time pass after 60s — let pm2 settle first.

← b0fcb62 yolo tick 18: embed-items --batch + boot-time drainer (5min/  ·  back to Whatsmystyle  ·  yolo tick 19: embed-progress live bar (30s poll + ETA) + pro 7bf8878 →