From ef8d075471ca55f471e947dc94d85ccb58841c06 Mon Sep 17 00:00:00 2001 From: bluedotiya <75704012+ChefControl@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:43:37 +0200 Subject: [PATCH 1/5] Scoreboards show "Pat S.", and a switch in Settings takes you off them (#213) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit publicName() now shows a first name and the initial of the last ("Pat Smith" is "Pat S."), never an email. It lives in src/publicName.ts so scripts/shorten-names.ts can shorten the names already up, run once by the owner with gcloud. The ☁️ menu gets a "Show me on the scoreboards" switch, on by default. Off takes the player's scores/{uid} and shame/{uid} down in one batch with the choice, kept on saves/{uid} as `hidden`, so every device follows it; nothing new goes up until it's back on. Saves are written merged so the choice stays. firestore.rules lets a signed-in player delete their own scores/ and shame/ docs, and allows `hidden` (a bool) on their save; rules tests cover both. Co-Authored-By: Claude Opus 5.5 --- firestore.rules | 14 ++++-- index.html | 30 +++++------ scripts/shorten-names.ts | 52 +++++++++++++++++++ src/cloud.ts | 71 ++++++++++++++++++++------ src/firebase.ts | 18 +++++-- src/publicName.ts | 16 ++++++ test/cloud.test.ts | 103 ++++++++++++++++++++++++++++++++++---- test/firebase.test.ts | 30 +++++++++-- test/rules/scores.test.ts | 63 +++++++++++++++++++++++ test/rules/shame.test.ts | 13 +++-- test/wallOfShame.test.ts | 4 +- 11 files changed, 355 insertions(+), 59 deletions(-) create mode 100644 scripts/shorten-names.ts create mode 100644 src/publicName.ts create mode 100644 test/rules/scores.test.ts diff --git a/firestore.rules b/firestore.rules index 41145d9..71ec2d1 100644 --- a/firestore.rules +++ b/firestore.rules @@ -1,9 +1,10 @@ rules_version = '2'; // Cloud saves (src/cloud.ts, src/firebase.ts): each player's save is one document, saves/{uid}, which only that -// signed-in player can read or write. The scoreboard (src/scores.ts) is one document per player, scores/{uid}: anyone -// can read it, a page of at most 50 at a time, and only that signed-in player can write it. The wall of shame -// (src/wallOfShame.ts) is the same, shame/{uid}: each player's worst bet, only ever swapped for a worse one. A bug +// signed-in player can read or write; it also says whether they keep off the scoreboards (`hidden`). The scoreboard +// (src/scores.ts) is one document per player, scores/{uid}: anyone can read it, a page of at most 50 at a time, and +// only that signed-in player can write it, or delete it to come off. The wall of shame (src/wallOfShame.ts) is the +// same, shame/{uid}: each player's worst bet, only ever swapped for a worse one, or deleted by them. A bug // report (src/report.ts) is a document in reports/, which a player signed in with Google can add but no player can // read, change or delete; the intake workflow (intake/) reads them with its own service account, which these rules // don't apply to. Each report goes with an update to limits/{uid}, which counts a player's reports: up to 20 in an @@ -15,10 +16,11 @@ service cloud.firestore { match /saves/{uid} { allow read: if request.auth != null && request.auth.uid == uid; allow create, update: if request.auth != null && request.auth.uid == uid - && request.resource.data.keys().hasOnly(['data', 'savedAt', 'updated']) + && request.resource.data.keys().hasOnly(['data', 'savedAt', 'updated', 'hidden']) && request.resource.data.data is string && request.resource.data.data.size() < 100000 - && request.resource.data.savedAt is number; + && request.resource.data.savedAt is number + && (!('hidden' in request.resource.data) || request.resource.data.hidden is bool); } match /scores/{uid} { allow get: if true; @@ -30,6 +32,7 @@ service cloud.firestore { && request.resource.data.best is number && request.resource.data.best >= 0 && request.resource.data.stage in [1, 2, 3]; + allow delete: if request.auth != null && request.auth.uid == uid; } match /shame/{uid} { allow get: if true; @@ -45,6 +48,7 @@ service cloud.firestore { && (!('on' in request.resource.data) || str(request.resource.data.on, 12)) && request.resource.data.broke is bool && request.resource.data.at is number; + allow delete: if request.auth != null && request.auth.uid == uid; } function googleUser(uid) { diff --git a/index.html b/index.html index 914e485..6ed4c23 100644 --- a/index.html +++ b/index.html @@ -457,20 +457,21 @@ #settings .cat[aria-expanded="true"] .chev{ transform:translateY(4px) rotate(-135deg); } #settings .catPanel{ display:flex; flex-direction:column; gap:8px; padding:8px 10px 10px; border-radius:14px; background:var(--chip); } #settings .catPanel[hidden]{ display:none; } -/* Controls opens into How to play (the controls card, hint.ts) and a switch for the Buy button on a computer */ +/* Controls opens into How to play (the controls card, hint.ts) and a switch for the Buy button on a computer; the + cloud's menu holds one for showing on the scoreboards (cloud.ts) */ #controls{ display:flex; align-items:center; min-height:40px; padding:4px 12px; border:0; border-radius:12px; background:var(--pill); color:var(--pill-ink); font:700 15px "Floe Emoji","Baloo 2","Trebuchet MS",system-ui,sans-serif; text-align:left; cursor:pointer; box-shadow:0 2px 0 var(--shade); } #controls i{ margin-left:auto; font-style:normal; font-size:20px; line-height:1; opacity:.6; } -#controlsPanel .toggle{ display:flex; align-items:center; gap:10px; padding:2px 2px 0 4px; font:700 15px "Floe Emoji","Baloo 2","Trebuchet MS",system-ui,sans-serif; } -#controlsPanel .toggle > span{ display:flex; flex-direction:column; line-height:1.15; min-width:0; } -#buyBtnNote{ font-size:12px; opacity:.6; } -#buyBtnNote:empty{ display:none; } -#buyBtnToggle{ position:relative; flex:none; margin-left:auto; width:52px; height:30px; padding:0; border:0; border-radius:999px; background:rgba(127,140,150,.45); cursor:pointer; transition:background .15s; } -#buyBtnToggle::after{ content:""; position:absolute; top:3px; left:3px; width:24px; height:24px; border-radius:50%; background:#fff; box-shadow:0 1px 3px rgba(0,0,0,.3); transition:transform .15s; } -#buyBtnToggle[aria-checked="true"]{ background:var(--coral); } -#buyBtnToggle[aria-checked="true"]::after{ transform:translateX(22px); } -#buyBtnToggle:disabled{ opacity:.55; cursor:default; } -#controls:focus-visible, #buyBtnToggle:focus-visible{ outline:3px solid var(--coral); outline-offset:2px; } -@media (prefers-reduced-motion: reduce){ #buyBtnToggle, #buyBtnToggle::after{ transition:none; } } +#settings .toggle{ display:flex; align-items:center; gap:10px; padding:2px 2px 0 4px; font:700 15px "Floe Emoji","Baloo 2","Trebuchet MS",system-ui,sans-serif; } +#settings .toggle > span{ display:flex; flex-direction:column; line-height:1.15; min-width:0; } +#settings .toggle small{ font-size:12px; opacity:.6; } +#settings .toggle small:empty{ display:none; } +#settings .switch{ position:relative; flex:none; margin:0 0 0 auto; width:52px; height:30px; min-height:0; padding:0; border:0; border-radius:999px; background:rgba(127,140,150,.45); cursor:pointer; transition:background .15s; } +#settings .switch::after{ content:""; position:absolute; top:3px; left:3px; width:24px; height:24px; border-radius:50%; background:#fff; box-shadow:0 1px 3px rgba(0,0,0,.3); transition:transform .15s; } +#settings .switch[aria-checked="true"]{ background:var(--coral); } +#settings .switch[aria-checked="true"]::after{ transform:translateX(22px); } +#settings .switch:disabled{ opacity:.55; cursor:default; } +#controls:focus-visible, #settings .switch:focus-visible{ outline:3px solid var(--coral); outline-offset:2px; } +@media (prefers-reduced-motion: reduce){ #settings .switch, #settings .switch::after{ transition:none; } } /* Sound opens into a mute button and a 0-10 slider for each kind of sound, sized for fingers */ /* Report a bug or idea (report.ts): the settings row (signed out, it signs in instead) opens a window of its own over the dimmed game, with a note, the picture of the game, a tickbox each for the picture and the game state, and a @@ -632,6 +633,7 @@ #cloudMenu{ display:flex; flex-direction:column; align-items:flex-start; gap:4px; padding:0 12px 4px; font-size:14px; line-height:1.25; } #cloudMenu[hidden]{ display:none; } #cloudMenu b{ font-weight:800; } +#cloudMenu .toggle{ align-self:stretch; margin-top:4px; padding:0; } #cloudMenu button{ margin-top:4px; font:800 15px "Floe Emoji","Baloo 2","Trebuchet MS",system-ui,sans-serif; background:var(--chip); color:var(--pill-ink); border:0; border-radius:999px; padding:4px 14px; cursor:pointer; } #cloudPick{ position:fixed; inset:0; z-index:8; display:grid; place-items:center; background:rgba(23,48,66,.55); } #cloudPick[hidden]{ display:none; } @@ -1485,7 +1487,7 @@