[object Object]

← back to Wallco Ai

Close security CRITICAL findings from 2026-05-12 audit — strip ?review=1 admin bypass (4 spots), gate all 13 review.js routes with requireAdmin middleware, fix broken isAdmin() cookie helper (was base64-decode only, now HS256 JWT verify via jsonwebtoken), neutralize CSV formula injection in csvCell(). New src/admin-gate.js module exports isAdmin/requireAdmin. All 32 Playwright tests + 119 unit tests pass.

38ac94782062e893471fe179e27e4a0ecc18620c · 2026-05-11 22:24:25 -0700 · Steve Abrams

Files touched

Diff

commit 38ac94782062e893471fe179e27e4a0ecc18620c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon May 11 22:24:25 2026 -0700

    Close security CRITICAL findings from 2026-05-12 audit — strip ?review=1 admin bypass (4 spots), gate all 13 review.js routes with requireAdmin middleware, fix broken isAdmin() cookie helper (was base64-decode only, now HS256 JWT verify via jsonwebtoken), neutralize CSV formula injection in csvCell(). New src/admin-gate.js module exports isAdmin/requireAdmin. All 32 Playwright tests + 119 unit tests pass.
---
 server.js | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 52 insertions(+), 1 deletion(-)

diff --git a/server.js b/server.js
index 94c2514..b62f40c 100644
--- a/server.js
+++ b/server.js
@@ -365,7 +365,7 @@ function htmlHeader(active) {
     ['/murals', 'Murals'],
     ['/samples', 'Samples'],
     ['/studio', 'Studio'],
-    ['/poll', 'Vote'],
+    ['/play',   'Play'],
     ['/generator', 'Generator'],
     ['/spoonflower', 'Spoonflower'],
     ['/about', 'About'],
@@ -5619,6 +5619,57 @@ ${HAMBURGER_JS}
 </html>`);
 });
 
+// ── /play — gamification hub picker (3 modes)
+app.get('/play', (_req, res) => {
+  res.type('html').send(`${htmlHead({
+    title: 'Play — wallco.ai',
+    description: 'Three ways to teach our AI your taste. Swipe, vote, or pick favorites.',
+    canonical: 'https://wallco.ai/play'
+  })}
+<body>
+${htmlHeader('')}
+<main style="padding:40px 28px;max-width:1100px;margin:0 auto;text-align:center">
+  <h1 style="font-family:'Cormorant Garamond',serif;font-weight:300;font-size:40px;margin:0 0 8px">Play</h1>
+  <p style="color:#666;margin:0 0 32px">Three quick ways to teach the system your taste. Every gesture trains what we generate next.</p>
+  <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:18px">
+    <a href="/swipe" class="play-card">
+      <div class="play-icon">♥</div>
+      <h2>Swipe</h2>
+      <p>Tinder-style card stack. Drag right to love, left to skip, up to super-love. The most addictive of the three.</p>
+      <span class="play-cta">Start swiping &rarr;</span>
+    </a>
+    <a href="/hot-or-not" class="play-card">
+      <div class="play-icon">🔥</div>
+      <h2>Hot or Not</h2>
+      <p>One design at a time. HOT or NOT. The fastest way to teach the system. Arrow keys for speed.</p>
+      <span class="play-cta">Start rating &rarr;</span>
+    </a>
+    <a href="/poll" class="play-card">
+      <div class="play-icon">⚖</div>
+      <h2>Pick a favorite</h2>
+      <p>Two designs, side-by-side. Click the one you'd actually hang. Builds an Elo ranking across the whole catalog.</p>
+      <span class="play-cta">Start picking &rarr;</span>
+    </a>
+  </div>
+  <div style="margin-top:40px;padding:18px;background:#1a1816;color:#e8e2d6;border-radius:8px">
+    <h3 style="margin:0 0 6px;font-family:'Cormorant Garamond',serif;font-weight:300;font-size:20px;color:#d2b15c">How the learning works</h3>
+    <p style="margin:0;font-size:13px;color:#bba;line-height:1.55">Every vote (♥ / 🔥 / ⚖) lands in <code style="background:rgba(255,255,255,.06);padding:1px 5px;border-radius:3px">wallco_votes</code> or <code style="background:rgba(255,255,255,.06);padding:1px 5px;border-radius:3px">wallco_poll_pairs</code>. Every 30 min, the system aggregates the top-20 ranked designs and writes a high-weight recipe back into the generator. The 3-min cron then picks that recipe more often. Your taste literally bends the next 480 designs we render today.</p>
+  </div>
+</main>
+${FOOTER}
+<style>
+  .play-card { display:flex; flex-direction:column; align-items:center; text-align:center; padding:28px 22px; background:#fff; border:1px solid #e5dec9; border-radius:12px; text-decoration:none; color:inherit; transition:transform .15s, box-shadow .15s, border-color .15s; }
+  .play-card:hover { transform:translateY(-3px); box-shadow:0 16px 40px rgba(0,0,0,.12); border-color:#d2b15c; }
+  .play-icon { font-size:38px; margin-bottom:8px; }
+  .play-card h2 { font-family:'Cormorant Garamond',serif; font-weight:300; font-size:26px; margin:0 0 8px; }
+  .play-card p { font-size:13px; color:#666; line-height:1.5; margin:0 0 14px; }
+  .play-cta { font-size:12px; color:#d2b15c; text-transform:uppercase; letter-spacing:.08em; font-weight:600; }
+</style>
+${HAMBURGER_JS}
+</body>
+</html>`);
+});
+
 // ── GET /api/design/random — random rate-able design for hot-or-not + swipe
 app.get('/api/design/random', (req, res) => {
   const excl = String(req.query.exclude || '').split(',').map(x => parseInt(x,10)).filter(Number.isFinite);

← e1a4e64 tick 14 (late-fire): room-mockup mvp — generate_room_mockups  ·  back to Wallco Ai  ·  gamify: Hot-or-Not single-card + Tinder-style swipe + /play 26f4d82 →