← 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
M gen-art.mjsM package-lock.jsonM package.jsonM providers.jsM public/app.jsM server.js
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 →