Compare commits
6 Commits
3c26eb4ba5
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1149687e3c | |||
| 3cf338d014 | |||
| 23ae455ec1 | |||
| 66a9e59db6 | |||
| ab069f2ec3 | |||
| cb664e4505 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "shelem",
|
"name": "shelem",
|
||||||
"version": "1.0.0",
|
"version": "1.1.7",
|
||||||
"description": "Shelem card game — multiplayer",
|
"description": "Shelem card game — multiplayer",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+33
-4
@@ -235,7 +235,7 @@ function show(id) { const el = $(id); if (el) el.classList.remove('hidden'); }
|
|||||||
function hide(id) { const el = $(id); if (el) el.classList.add('hidden'); }
|
function hide(id) { const el = $(id); if (el) el.classList.add('hidden'); }
|
||||||
|
|
||||||
function cardSvg(code) {
|
function cardSvg(code) {
|
||||||
if (code === 'JOKER-COLOR') return '/cards/JOKER-1.svg';
|
if (code === 'JOKER-COLOR') return '/cards/JOKER-3.svg';
|
||||||
if (code === 'JOKER-BLACK') return '/cards/JOKER-2.svg';
|
if (code === 'JOKER-BLACK') return '/cards/JOKER-2.svg';
|
||||||
const [suit, rank] = code.split('-');
|
const [suit, rank] = code.split('-');
|
||||||
const suitMap = { C: 'CLUB', D: 'DIAMOND', H: 'HEART', S: 'SPADE' };
|
const suitMap = { C: 'CLUB', D: 'DIAMOND', H: 'HEART', S: 'SPADE' };
|
||||||
@@ -1037,7 +1037,7 @@ function renderWidowOverlay(st) {
|
|||||||
updateWidowPreview(st);
|
updateWidowPreview(st);
|
||||||
}
|
}
|
||||||
|
|
||||||
const WIDOW_SUIT_ORDER = ['C', 'D', 'H', 'S'];
|
const WIDOW_SUIT_ORDER = ['C', 'D', 'S', 'H'];
|
||||||
const WIDOW_SUIT_LABEL = { C: '♣', D: '♦', H: '♥', S: '♠' };
|
const WIDOW_SUIT_LABEL = { C: '♣', D: '♦', H: '♥', S: '♠' };
|
||||||
const WIDOW_JOKER_LABEL = { 'JOKER-COLOR': '🌈', 'JOKER-BLACK': '⚫' };
|
const WIDOW_JOKER_LABEL = { 'JOKER-COLOR': '🌈', 'JOKER-BLACK': '⚫' };
|
||||||
|
|
||||||
@@ -1179,7 +1179,7 @@ function onHandOver(st) {
|
|||||||
const isWinner = mySeat >= 0 && teamOf(mySeat) === team && delta > 0;
|
const isWinner = mySeat >= 0 && teamOf(mySeat) === team && delta > 0;
|
||||||
if (isWinner) row.classList.add('winner');
|
if (isWinner) row.classList.add('winner');
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<span class="team-label">${names}</span>
|
<span class="team-label">${escHtml(names)}</span>
|
||||||
<span class="delta ${delta >= 0 ? 'pos' : 'neg'}">${delta >= 0 ? '+' : ''}${delta}</span>
|
<span class="delta ${delta >= 0 ? 'pos' : 'neg'}">${delta >= 0 ? '+' : ''}${delta}</span>
|
||||||
<span style="color:rgba(255,255,255,.6);font-size:.8rem">→ ${st.scores[team]}</span>
|
<span style="color:rgba(255,255,255,.6);font-size:.8rem">→ ${st.scores[team]}</span>
|
||||||
`;
|
`;
|
||||||
@@ -1211,7 +1211,7 @@ function onGameOver(st) {
|
|||||||
.filter(Boolean).join(' & ') || `Team ${team + 1}`;
|
.filter(Boolean).join(' & ') || `Team ${team + 1}`;
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'gameover-row' + (winTeams.has(team) ? ' winner' : '');
|
row.className = 'gameover-row' + (winTeams.has(team) ? ' winner' : '');
|
||||||
row.innerHTML = `<span>${names}</span><span style="font-weight:700;color:${team===0?'var(--team-a)':'var(--team-b)'}">${st.scores[team]}</span>`;
|
row.innerHTML = `<span>${escHtml(names)}</span><span style="font-weight:700;color:${team===0?'var(--team-a)':'var(--team-b)'}">${st.scores[team]}</span>`;
|
||||||
scoresEl.appendChild(row);
|
scoresEl.appendChild(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1700,11 +1700,40 @@ async function validateAuth() {
|
|||||||
updateAuthBar();
|
updateAuthBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show the running app version in the lobby footer (served from /api/config).
|
||||||
|
// Tapping it force-updates: clears the service-worker caches and reloads, so a
|
||||||
|
// phone stuck on stale cached assets can pull the latest build.
|
||||||
|
async function loadAppVersion() {
|
||||||
|
try {
|
||||||
|
const d = await fetch('/api/config').then(r => r.json());
|
||||||
|
if (d.version) $('app-version').textContent = `v${d.version} · tap to update`;
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function forceUpdate() {
|
||||||
|
const el = $('app-version');
|
||||||
|
if (el) el.textContent = 'Updating…';
|
||||||
|
try {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
const regs = await navigator.serviceWorker.getRegistrations();
|
||||||
|
await Promise.all(regs.map(r => r.unregister()));
|
||||||
|
}
|
||||||
|
if (window.caches) {
|
||||||
|
const keys = await caches.keys();
|
||||||
|
await Promise.all(keys.map(k => caches.delete(k)));
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
// SW gone and caches cleared — a plain reload now refetches everything fresh
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Boot ─────────────────────────────────────────────────────
|
// ─── Boot ─────────────────────────────────────────────────────
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
initLobby();
|
initLobby();
|
||||||
wireGameEvents();
|
wireGameEvents();
|
||||||
applyBarBottom();
|
applyBarBottom();
|
||||||
|
loadAppVersion();
|
||||||
|
$('app-version').addEventListener('click', forceUpdate);
|
||||||
|
|
||||||
// Drop any stale/corrupted auth token before we use it to connect
|
// Drop any stale/corrupted auth token before we use it to connect
|
||||||
await validateAuth();
|
await validateAuth();
|
||||||
|
|||||||
@@ -91,6 +91,7 @@
|
|||||||
|
|
||||||
<p id="lobby-error" class="error-msg"></p>
|
<p id="lobby-error" class="error-msg"></p>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="app-version" class="app-version"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ══════════════ WAITING ROOM ══════════════ -->
|
<!-- ══════════════ WAITING ROOM ══════════════ -->
|
||||||
|
|||||||
+16
-2
@@ -1033,6 +1033,20 @@ input:focus { border-color: var(--gold); }
|
|||||||
.lb-table td { border-bottom: 1px solid rgba(255,255,255,.05); }
|
.lb-table td { border-bottom: 1px solid rgba(255,255,255,.05); }
|
||||||
.lb-table tr:hover td { background: rgba(255,255,255,.04); }
|
.lb-table tr:hover td { background: rgba(255,255,255,.04); }
|
||||||
|
|
||||||
|
/* ── App version footer ───── */
|
||||||
|
.app-version {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 14px;
|
||||||
|
font-size: .72rem;
|
||||||
|
color: rgba(255,255,255,.35);
|
||||||
|
letter-spacing: .5px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.app-version:hover { color: rgba(255,255,255,.6); text-decoration: underline; }
|
||||||
|
.app-version:active { color: var(--gold); }
|
||||||
|
|
||||||
/* ── Round-by-round breakdown ───── */
|
/* ── Round-by-round breakdown ───── */
|
||||||
.round-breakdown-wrap { margin: 6px 0 2px; }
|
.round-breakdown-wrap { margin: 6px 0 2px; }
|
||||||
.rb-scroll { max-height: 42vh; overflow-y: auto; -webkit-overflow-scrolling: touch; }
|
.rb-scroll { max-height: 42vh; overflow-y: auto; -webkit-overflow-scrolling: touch; }
|
||||||
@@ -1269,8 +1283,8 @@ input:focus { border-color: var(--gold); }
|
|||||||
box-shadow: 0 6px 28px rgba(0,0,0,.8);
|
box-shadow: 0 6px 28px rgba(0,0,0,.8);
|
||||||
max-height: 55vh;
|
max-height: 55vh;
|
||||||
}
|
}
|
||||||
/* Shrink bid history on mobile so it doesn't swallow the screen */
|
/* Always 4 players, so show all four bids without an inner scroll */
|
||||||
.bid-history { max-height: 110px; }
|
.bid-history { max-height: none; }
|
||||||
.bid-box h3 { font-size: .95rem; margin-bottom: 0; }
|
.bid-box h3 { font-size: .95rem; margin-bottom: 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
const CACHE_NAME = 'shelem-v2';
|
const CACHE_NAME = 'shelem-v3';
|
||||||
|
|
||||||
// ── Generate all 55 card paths ──────────────────────────────────
|
// ── Generate all 55 card paths ──────────────────────────────────
|
||||||
const SUITS = ['CLUB', 'DIAMOND', 'HEART', 'SPADE'];
|
const SUITS = ['CLUB', 'DIAMOND', 'HEART', 'SPADE'];
|
||||||
|
|||||||
@@ -10,6 +10,17 @@ const bcrypt = require('bcryptjs');
|
|||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
app.disable('x-powered-by'); // don't advertise the tech stack
|
||||||
|
|
||||||
|
// Safe security headers (no CSP here — inline SW script + socket.io would need
|
||||||
|
// 'unsafe-inline'/wss exceptions; Cloudflare already terminates TLS/HSTS)
|
||||||
|
app.use((_req, res, next) => {
|
||||||
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||||
|
res.setHeader('X-Frame-Options', 'DENY');
|
||||||
|
res.setHeader('Referrer-Policy', 'no-referrer');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
const httpServer = http.createServer(app);
|
const httpServer = http.createServer(app);
|
||||||
|
|
||||||
let httpsServer = null;
|
let httpsServer = null;
|
||||||
@@ -132,8 +143,10 @@ app.use(express.static(path.join(__dirname, 'public'), {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// ─── Auth API ─────────────────────────────────────────────────
|
// ─── Auth API ─────────────────────────────────────────────────
|
||||||
|
const APP_VERSION = require('./package.json').version;
|
||||||
|
|
||||||
app.get('/api/config', (_req, res) => {
|
app.get('/api/config', (_req, res) => {
|
||||||
res.json({ signupsOpen: config.signupsOpen });
|
res.json({ signupsOpen: config.signupsOpen, version: APP_VERSION });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validate the stored auth token; the client uses this at startup to detect a
|
// Validate the stored auth token; the client uses this at startup to detect a
|
||||||
@@ -349,7 +362,8 @@ function trumpRank(card) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Suit display order: C D H S JOKER
|
// Suit display order: C D H S JOKER
|
||||||
const SUIT_ORD = { C: 0, D: 1, H: 2, S: 3, JOKER: 4 };
|
// Alternate colours (black-red-black-red) so same-colour suits aren't adjacent
|
||||||
|
const SUIT_ORD = { C: 0, D: 1, S: 2, H: 3, JOKER: 4 };
|
||||||
function sortCards(hand) {
|
function sortCards(hand) {
|
||||||
return [...hand].sort((a, b) => {
|
return [...hand].sort((a, b) => {
|
||||||
const sa = SUIT_ORD[suitOf(a)], sb = SUIT_ORD[suitOf(b)];
|
const sa = SUIT_ORD[suitOf(a)], sb = SUIT_ORD[suitOf(b)];
|
||||||
|
|||||||
Reference in New Issue
Block a user