[object Object]

← back to Model Wars

chore: lint, refactor, v1.1.0 (session close)

8b56730334db609066ad5ba7f7360228ff842232 · 2026-08-24 09:56:12 -0700 · Steve Abrams

- fix: /api/battle resolves from allChampions() so custom champions can battle
- guard: boot() catches champions-fetch failure; vote buttons null-safe
- refactor: drop dead node:process import, optional chaining, ternary cleanup
- version 1.0.0 → 1.1.0

Files touched

Diff

commit 8b56730334db609066ad5ba7f7360228ff842232
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 24 09:56:12 2026 -0700

    chore: lint, refactor, v1.1.0 (session close)
    
    - fix: /api/battle resolves from allChampions() so custom champions can battle
    - guard: boot() catches champions-fetch failure; vote buttons null-safe
    - refactor: drop dead node:process import, optional chaining, ternary cleanup
    - version 1.0.0 → 1.1.0
---
 gen-art.mjs       |  2 +-
 package-lock.json |  4 ++--
 package.json      |  2 +-
 providers.js      |  2 --
 public/app.js     | 16 +++++++++-------
 server.js         |  5 +++--
 6 files changed, 16 insertions(+), 15 deletions(-)

diff --git a/gen-art.mjs b/gen-art.mjs
index d20ac3b..3a78bc6 100644
--- a/gen-art.mjs
+++ b/gen-art.mjs
@@ -42,7 +42,7 @@ async function gen(job) {
   });
   let pred = await start.json();
   if (pred.error) throw new Error(pred.error);
-  while (pred.status !== 'succeeded' && pred.status !== 'failed' && pred.status !== 'canceled') {
+  while (!['succeeded', 'failed', 'canceled'].includes(pred.status)) {
     await new Promise((r) => setTimeout(r, 1500));
     pred = await fetch(pred.urls.get, { headers: { Authorization: `Bearer ${TOKEN}` } }).then((x) => x.json());
   }
diff --git a/package-lock.json b/package-lock.json
index 877bacc..c49eea9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "model-wars",
-  "version": "1.0.0",
+  "version": "1.1.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "model-wars",
-      "version": "1.0.0",
+      "version": "1.1.0",
       "license": "MIT",
       "dependencies": {
         "express": "^4.19.2"
diff --git a/package.json b/package.json
index e5ec320..4ffa299 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "model-wars",
-  "version": "1.0.0",
+  "version": "1.1.0",
   "description": "MODEL WARS — leading AI models battle head-to-head in a medieval tournament. Real API battles, AI judges, and a persistent medieval ELO ladder.",
   "type": "module",
   "main": "server.js",
diff --git a/providers.js b/providers.js
index cebb05d..ef1c773 100644
--- a/providers.js
+++ b/providers.js
@@ -3,8 +3,6 @@
 // No adapter ever fabricates output: if a key is missing or a call fails,
 // the caller learns the truth and the UI blocks live battle for that champion.
 
-import 'node:process';
-
 const env = process.env;
 
 // Pricing in USD per 1,000,000 tokens: [input, output]. Best-effort public
diff --git a/public/app.js b/public/app.js
index ce5f98f..531d1f0 100644
--- a/public/app.js
+++ b/public/app.js
@@ -37,7 +37,9 @@
 
   // ── boot ───────────────────────────────────────────────
   async function boot() {
-    const r = await fetch('/api/champions').then((x) => x.json());
+    let r;
+    try { r = await fetch('/api/champions').then((x) => x.json()); }
+    catch (e) { toast('Failed to load champions — is the server running? ' + e.message); return; }
     CHAMPS = r.champions; JUDGE = r.judge;
     const liveCount = CHAMPS.filter((c) => c.available).length;
     const flag = $('#modeFlag'), txt = $('#modeText');
@@ -70,8 +72,8 @@
     else { portrait.style.display = 'none'; crest.style.display = ''; }
     crest.textContent = c.crest;
     crest.style.filter = `drop-shadow(0 0 14px ${c.color})`;
-    box.querySelector('.cname').textContent = c.name;
-    box.querySelector('.cname').style.color = c.color;
+    const cname = box.querySelector('.cname');
+    cname.textContent = c.name; cname.style.color = c.color;
     box.querySelector('.ctitle').textContent = c.title;
     const av = box.querySelector('.avail');
     av.className = 'avail ' + (c.available ? 'on' : 'off');
@@ -266,7 +268,7 @@
     const t = JSON.parse(localStorage.getItem(peopleKey()) || '{}');
     const na = t[last.a.id] || 0, nb = t[last.b.id] || 0, tot = na + nb;
     $('#peoplesChoice').innerHTML = tot
-      ? `👥 People's Choice for this matchup — <b style="color:${last.a.color}">${last.a.name} ${tot ? Math.round(na / tot * 100) : 0}%</b> vs <b style="color:${last.b.color}">${last.b.name} ${tot ? Math.round(nb / tot * 100) : 0}%</b> (${tot} votes) · kept separate from the AI Score`
+      ? `👥 People's Choice for this matchup — <b style="color:${last.a.color}">${last.a.name} ${Math.round(na / tot * 100)}%</b> vs <b style="color:${last.b.color}">${last.b.name} ${Math.round(nb / tot * 100)}%</b> (${tot} votes) · kept separate from the AI Score`
       : `👥 No public votes yet for this matchup — cast yours above. People's Choice is tallied separately from the AI Score.`;
     $('#voteA').textContent = `🗳 Vote ${last.a.name}`;
     $('#voteB').textContent = `🗳 Vote ${last.b.name}`;
@@ -288,7 +290,7 @@
     $('#ladderBody').innerHTML = rows.map((r) => `
       <tr class="${r.rank === 1 ? 'rank1' : ''}">
         <td>${r.rank === 1 ? '<span class="crown">👑</span> ' : ''}${r.rank}</td>
-        <td><div class="champcell">${(champById(r.id) && champById(r.id).img) ? `<img class="avatar" src="${champById(r.id).img}" alt="" style="box-shadow:0 0 10px ${r.color}66">` : `<span class="cr" style="filter:drop-shadow(0 0 8px ${r.color})">${r.crest}</span>`}
+        <td><div class="champcell">${champById(r.id)?.img ? `<img class="avatar" src="${champById(r.id).img}" alt="" style="box-shadow:0 0 10px ${r.color}66">` : `<span class="cr" style="filter:drop-shadow(0 0 8px ${r.color})">${r.crest}</span>`}
           <span><div class="nm" style="color:${r.color}">${r.name}</div><div class="tt">${r.title}</div></span></div></td>
         <td>${r.provider}</td>
         <td class="elo">${r.elo}</td>
@@ -306,8 +308,8 @@
   $('#selA').onchange = () => renderFighter('A');
   $('#selB').onchange = () => renderFighter('B');
   $('#beginBtn').onclick = begin;
-  $('#voteA').onclick = () => vote(last.a.id);
-  $('#voteB').onclick = () => vote(last.b.id);
+  $('#voteA').onclick = () => { if (last) vote(last.a.id); };
+  $('#voteB').onclick = () => { if (last) vote(last.b.id); };
   $('#evidenceBtn').onclick = () => {
     const shown = $$('#results .response-full').some((e) => e.style.display === 'block');
     $$('#results .response-full').forEach((e) => e.style.display = shown ? 'none' : 'block');
diff --git a/server.js b/server.js
index d0e0c9b..cf66c3d 100644
--- a/server.js
+++ b/server.js
@@ -161,8 +161,9 @@ app.post('/api/champions', async (req, res) => {
 app.post('/api/battle', async (req, res) => {
   const { prompt, a, b } = req.body || {};
   if (!prompt || !a || !b) return res.status(400).json({ error: 'prompt, a, b required' });
-  const champA = CHAMPIONS.find((c) => c.id === a);
-  const champB = CHAMPIONS.find((c) => c.id === b);
+  const roster = await allChampions();               // include custom champions
+  const champA = roster.find((c) => c.id === a);
+  const champB = roster.find((c) => c.id === b);
   if (!champA || !champB) return res.status(400).json({ error: 'unknown champion' });
 
   const run = async (champ) => {

← cb1a8df 5x verification: six-way core 6/6 PASS; add sweep report  ·  back to Model Wars  ·  feat: procedural Web Audio battle sound + mute toggle (v1.2. 75a84ba →