← back to Whatsmystyle
yolo tick 13: couples + embedding_drifts schema; closet.privacy col; /admin-config UI
477ba99210edeb3d10cd0c2344e70a4150728374 · 2026-05-12 01:34:28 -0700 · SteveStudio2
Files touched
A public/admin-config.htmlM server.js
Diff
commit 477ba99210edeb3d10cd0c2344e70a4150728374
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Tue May 12 01:34:28 2026 -0700
yolo tick 13: couples + embedding_drifts schema; closet.privacy col; /admin-config UI
---
public/admin-config.html | 105 +++++++++++++++++++++++++++++++++++++++++++++++
server.js | 43 ++++++++++++++++++-
2 files changed, 146 insertions(+), 2 deletions(-)
diff --git a/public/admin-config.html b/public/admin-config.html
new file mode 100644
index 0000000..97e7e21
--- /dev/null
+++ b/public/admin-config.html
@@ -0,0 +1,105 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <meta name="robots" content="noindex,nofollow" />
+ <title>WhatsMyStyle — Admin Config</title>
+ <link rel="stylesheet" href="/css/app.css" />
+ <style>
+ body { font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; background: #faf7f2; color: #1d1d1f; max-width: 720px; margin: 40px auto; padding: 0 24px; }
+ h1 { font-size: 32px; font-weight: 600; letter-spacing: -0.02em; margin: 0 0 8px; }
+ .sub { color: #707070; margin-bottom: 32px; }
+ .pill-row { background: #fff; border: 1px solid #e6e1d8; border-radius: 24px; padding: 20px 24px; margin-bottom: 14px; display: flex; align-items: center; gap: 16px; }
+ .pill-row label { flex: 1; }
+ .pill-row .name { font-weight: 600; font-size: 16px; display: block; margin-bottom: 4px; }
+ .pill-row .hint { font-size: 13px; color: #707070; }
+ .pill-row input[type=number], .pill-row input[type=text] { font: inherit; padding: 10px 16px; border: 1px solid #d6d0c4; border-radius: 14px; width: 140px; text-align: right; background: #faf7f2; }
+ .pill-row input[type=checkbox] { width: 22px; height: 22px; }
+ button.save { font: inherit; font-weight: 600; background: #1d1d1f; color: #fff; border: 0; padding: 14px 28px; border-radius: 999px; cursor: pointer; margin-top: 8px; }
+ button.save:hover { background: #3a3a3c; }
+ .status { padding: 12px 16px; border-radius: 12px; margin-top: 16px; font-size: 14px; }
+ .status.ok { background: #e6f6ec; color: #14572a; }
+ .status.err { background: #fbe4e4; color: #7a1717; }
+ .nav { margin-bottom: 24px; }
+ .nav a { color: #707070; text-decoration: none; font-size: 14px; }
+ .nav a:hover { color: #1d1d1f; }
+ </style>
+</head>
+<body>
+ <div class="nav"><a href="/">← back to app</a></div>
+ <h1>Admin config</h1>
+ <p class="sub">Knobs the duel engine + budget guard read on every request. Saved values persist in <code>app_config</code>.</p>
+
+ <form id="cfg">
+ <div class="pill-row">
+ <label>
+ <span class="name">Cold-start cutoff</span>
+ <span class="hint">How many duels before we drop budget anchoring and go pure-random. Default 3.</span>
+ </label>
+ <input type="number" min="0" max="50" step="1" name="cold_start_cutoff" id="cold_start_cutoff" />
+ </div>
+
+ <div class="pill-row">
+ <label>
+ <span class="name">Outlier injection rate</span>
+ <span class="hint">Probability that during cold-start we swap one item for a random outlier. 0–1. Default 0.2.</span>
+ </label>
+ <input type="number" min="0" max="1" step="0.05" name="outlier_injection_rate" id="outlier_injection_rate" />
+ </div>
+
+ <div class="pill-row">
+ <label>
+ <span class="name">Per-user render budget (cents)</span>
+ <span class="hint">Hard cap on paid try-on calls per user. 500 = $5. Default 500.</span>
+ </label>
+ <input type="number" min="0" step="50" name="budget_cents_per_user" id="budget_cents_per_user" />
+ </div>
+
+ <div class="pill-row">
+ <label>
+ <span class="name">Couples pilot</span>
+ <span class="hint">Feature flag for the shared-closet pilot. DB scaffold ready; UI ships in a later tick. Default off.</span>
+ </label>
+ <input type="checkbox" name="couples_pilot" id="couples_pilot" />
+ </div>
+
+ <button class="save" type="submit">Save</button>
+ <div id="status"></div>
+ </form>
+
+ <script>
+ const $ = (id) => document.getElementById(id);
+ const isAdminQ = location.search.includes('admin=1') ? '?admin=1' : '';
+
+ async function load() {
+ const r = await fetch('/api/admin/config' + isAdminQ);
+ if (!r.ok) { $('status').className = 'status err'; $('status').textContent = 'admin gate failed'; return; }
+ const { config, defaults } = await r.json();
+ $('cold_start_cutoff').value = config.cold_start_cutoff ?? defaults.cold_start_cutoff;
+ $('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;
+ }
+
+ document.getElementById('cfg').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const body = {
+ cold_start_cutoff: parseInt($('cold_start_cutoff').value, 10),
+ outlier_injection_rate: parseFloat($('outlier_injection_rate').value),
+ budget_cents_per_user: parseInt($('budget_cents_per_user').value, 10),
+ couples_pilot: $('couples_pilot').checked,
+ };
+ const r = await fetch('/api/admin/config' + isAdminQ, {
+ method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body),
+ });
+ const j = await r.json();
+ const s = $('status');
+ if (r.ok) { s.className = 'status ok'; s.textContent = 'Saved: ' + (j.written || []).join(', '); }
+ else { s.className = 'status err'; s.textContent = j.error || 'save failed'; }
+ });
+
+ load();
+ </script>
+</body>
+</html>
diff --git a/server.js b/server.js
index b123946..f651e25 100644
--- a/server.js
+++ b/server.js
@@ -206,8 +206,40 @@ CREATE TABLE IF NOT EXISTS debates (
panel TEXT, -- JSON array of {role, model, output}
created_at TEXT DEFAULT (datetime('now'))
);
+
+-- Tick 13: couples / shared-closets scaffold per debate id 8 verdict (HOLD - pilot).
+-- DB-only this tick; gated behind app_config.couples_pilot (default false).
+CREATE TABLE IF NOT EXISTS couples (
+ id INTEGER PRIMARY KEY,
+ partner_a_user_id INTEGER NOT NULL,
+ partner_b_user_id INTEGER NOT NULL,
+ status TEXT NOT NULL DEFAULT 'pending', -- pending | active | exited
+ opt_in_at TEXT, -- both partners confirmed
+ exit_at TEXT, -- breakup flow; on set, cross-twin training is purged
+ created_at TEXT DEFAULT (datetime('now')),
+ UNIQUE(partner_a_user_id, partner_b_user_id)
+);
+CREATE INDEX IF NOT EXISTS couples_a_idx ON couples(partner_a_user_id);
+CREATE INDEX IF NOT EXISTS couples_b_idx ON couples(partner_b_user_id);
+
+-- Tick 13: catalog re-embed drift ledger. One row per (item, run) where
+-- new vs stored similarity < 0.80. checked_at is second-precision so the
+-- UNIQUE constraint blocks dupes within the same drift sweep second.
+CREATE TABLE IF NOT EXISTS embedding_drifts (
+ id INTEGER PRIMARY KEY,
+ item_id INTEGER NOT NULL,
+ old_vec TEXT NOT NULL,
+ new_vec TEXT NOT NULL,
+ similarity REAL NOT NULL,
+ checked_at TEXT NOT NULL DEFAULT (datetime('now')),
+ UNIQUE(item_id, checked_at)
+);
+CREATE INDEX IF NOT EXISTS embedding_drifts_item_idx ON embedding_drifts(item_id, id);
`);
+// Tick 13: closet.privacy column (default 'shared'). Idempotent.
+safeAddColumn('closet', "privacy TEXT DEFAULT 'shared'");
+
// ---------- app ------------------------------------------------------------
const app = express();
app.use(express.json({ limit: '5mb' }));
@@ -487,13 +519,20 @@ 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 },
+ defaults: { cold_start_cutoff: 3, outlier_injection_rate: 0.2, budget_cents_per_user: 500, couples_pilot: false },
});
});
+
+// ---- admin / config UI (tick 13) ------------------------------------------
+// Plain large-pill form. Same gate as /api/admin/config.
+app.get('/admin-config', (req, res) => {
+ if (!adminGate(req)) return res.status(403).send('admin only');
+ res.sendFile(path.join(__dirname, 'public', 'admin-config.html'));
+});
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'];
+ const ALLOWED = ['cold_start_cutoff', 'outlier_injection_rate', 'budget_cents_per_user', 'couples_pilot'];
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 = [];
← 543e924 yolo tick 12: couples-closets debate verdict (HOLD - pilot w
·
back to Whatsmystyle
·
yolo tick 13: catalog re-embed drift checker (cosine vs stor 30fe227 →