diff --git a/DESIGN.md b/DESIGN.md index 26a5153..9d7e002 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,7 +1,7 @@ # PixFray design Applies to every page PixFray renders: the home page tour (`/`), the picker (`/play/`), a channel's fighter page -(`/?channel=`), `/start/`, the mod page (`/admin/`), the owner page, the not-found page and the OBS overlay. +(`/?channel=`), `/start/`, `/changelog/`, the mod page (`/admin/`), the owner page, the not-found page and the OBS overlay. The look is a **night arena tavern**: warm near-black wood, one gold trim accent, Fraunces headings, and the 3D arena scene behind the page. Everything here is already built in `public/dashboard.css` (pages) and `src/intro/intro.css` (home page); this file says how to use it. Read it before changing any page. @@ -14,6 +14,7 @@ Write one line for yourself before laying anything out: who opens this page and |---|---|---| | `/` | A viewer or streamer who heard of PixFray: learn what it is | The 3D tour and one way in | | `/play/` | A viewer: find the stream they watch | "Which stream are you watching?", the search, the channel rows | +| `/changelog/` | A viewer or streamer: see what changed and when | The current version, then releases newest first | | `/?channel=` | A viewer: build their fighter and save it | The 3D fighter stage, the character grid, **Save profile** | | `/start/` | A streamer: set PixFray up | The headline, **Sign in with Twitch**, the live overlay demo | | `/admin/` | A mod or the broadcaster: run duels live | The live state, Pause duels, the setup checklist | diff --git a/admin/dev/index.html b/admin/dev/index.html index 4dc40e3..a39ad67 100644 --- a/admin/dev/index.html +++ b/admin/dev/index.html @@ -229,7 +229,7 @@

Integrations and what each one needs

diff --git a/admin/index.html b/admin/index.html index 811c2e8..246d1a5 100644 --- a/admin/index.html +++ b/admin/index.html @@ -325,7 +325,7 @@

Turn PixFray off

diff --git a/changelog/index.html b/changelog/index.html new file mode 100644 index 0000000..448ab70 --- /dev/null +++ b/changelog/index.html @@ -0,0 +1,174 @@ + + +What's new in PixFray ยท Changelog + + + + + + +
+ PixFray + +
+ + +
+ + +
+ + +
+
+

0.8.0 Channel point rewards

+

+
    +
  • New Channel point rewards that add PixFray dollars to the redeemer's fighter, up to 5 per channel. The bot confirms each one in chat with the new wallet total. Setting them up from the mod page comes in a later release.
  • +
  • New A viewer who redeems without a saved fighter gets their points back. A repeated delivery from Twitch never pays twice.
  • +
  • New This changelog page.
  • +
  • Under the hood Duel, alarm and chat logs and daily room counters, so problems are easier to trace.
  • +
+
+ +
+

0.7.1 Steadier rooms

+

+
    +
  • Fixed An expired rematch no longer keeps a channel's room waking up over and over.
  • +
+
+ +
+

0.7.0 Deeper fighter stats

+

+
    +
  • New Upgrade points have no cap. Each level costs more, every stat has a ceiling, and presets spend points for you.
  • +
  • New Crit, dodge and counter stats, plus vitality.
  • +
  • New Legendary stat rolls with one free reroll, and !stats to see your fighter's numbers in chat.
  • +
  • New Turning a picture into a sprite works without AI: PixFray finds the subject and drops the background.
  • +
  • Changed Duels are ranked only while the stream is live. Streamers can also limit how often the same two fighters duel in one stream.
  • +
  • Fixed The bot no longer stalls on an unreadable shop answer.
  • +
+
+ +
+

0.6.2 Under the hood

+

+
    +
  • Under the hood All of PixFray now runs on strict TypeScript, and everything it receives is checked before use.
  • +
  • Fixed A stray empty slot after full build slots, and the mod page on a channel without an arena.
  • +
+
+ +
+

0.6.1 Ranks and backups

+

+
    +
  • Fixed Saving your fighter keeps your rank.
  • +
  • Fixed The !fray link opens your channel instead of the channel picker.
  • +
  • New Every channel is backed up daily and kept for 90 days, and a lost rank can be restored.
  • +
+
+ +
+

0.6.0 Viewer sprites and bot controls

+

+
    +
  • New Turn any picture into a pixel fighter, with an optional AI redraw. Mods review each one before it goes on stream.
  • +
  • New !fray off and !fray on silence or wake the bot in a channel.
  • +
  • New Mods add the bot's own text commands, with counters, and can turn on a timed !fray reminder.
  • +
  • New !give sends dollars (!pay still works), and an unanswered challenge gets an expiry line.
  • +
  • Changed The bot names the duel winner in chat, and the overlay no longer shows a winner banner.
  • +
  • Changed Duels show 2D hit effects: glow, sparks, shake and a knockout push-in. The overlay option fx=off turns them off, and bubbles=0 hides chat bubbles.
  • +
  • Changed Duels with a brand-new Twitch account are unrated, and the same two fighters have a daily ranked limit.
  • +
  • Changed The home page is the 3D tour, and the channel picker moved to /play/.
  • +
  • Fixed Lead moderators count as mods, and a fighter changed on the website updates on stream.
  • +
+
+ +
+

0.5.0 Open sign-up and the PixFray bot

+

+
    +
  • New Any streamer can turn PixFray on from /start/, with no invite.
  • +
  • New The PixFray bot answers duel commands in chat, so streamers need no other chat bot.
  • +
  • New Search the channel picker for any channel running PixFray.
  • +
  • New Mods can try !checkin for 15 minutes while offline.
  • +
  • New Anyone can run their own copy of PixFray from one config file.
  • +
  • Changed Shop prices cut to about a third.
  • +
+
+ +
+

0.4.0 PixFray

+

+
    +
  • Changed Mini Chat is now PixFray, and the help command is !fray.
  • +
  • New Build slots and cosmetics, on a fighter page with tabs for the shop, Ranks and Rules.
  • +
  • New A 3D preview of your fighter, and a 3D tour of how a duel plays out.
  • +
  • New !look links straight to picking or changing your fighter.
  • +
  • Changed Every page redesigned as a night arena.
  • +
+
+ +
+

0.3.0 Rematches and progression

+

+
    +
  • New !rematch challenges your last opponent.
  • +
  • New !checkin and stream streaks.
  • +
  • New Dollars, with !wallet, !pay and gifts from mods.
  • +
  • New Pets and the shop.
  • +
  • Changed Duel results stay out of chat until the stream has shown them.
  • +
+
+ +
+

0.2.0 Upgrades and more fighters

+

+
    +
  • New Upgrades earned from wins (power, guard and luck) and hats to earn.
  • +
  • New 56 built-in characters, and up to 24 uploaded characters per channel.
  • +
  • New !top and !elo, and a limit on how many fighters show on stream.
  • +
  • New Streamer sign-up, with a setup checklist that ticks off each step as it starts working.
  • +
  • New Optional duel announcements on the overlay.
  • +
+
+ +
+

0.1.0 First duels

+

+
    +
  • New Chatters walk on stream as pixel fighters through an OBS overlay.
  • +
  • New Duels with !challenge @name and !fight, settled by alternating d6 rolls with crits.
  • +
  • New Elo, wins and losses saved for every fighter.
  • +
  • New The overlay replays each duel: the fighters meet, HP bars drop with each roll, and the loser is knocked out.
  • +
  • New A fighter page for viewers and duel controls for mods.
  • +
+
+
+
+
+ + diff --git a/cloudflare.config.ts b/cloudflare.config.ts index 3d7665f..2fcb8cb 100644 --- a/cloudflare.config.ts +++ b/cloudflare.config.ts @@ -41,6 +41,8 @@ export default defineConfig(({ mode }) => { "/start/*", "/play", "/play/*", + "/changelog", + "/changelog/*", "/intro", "/intro/*", "/robots.txt", diff --git a/docs/CONTRACTS.md b/docs/CONTRACTS.md index 5109213..94c7492 100644 --- a/docs/CONTRACTS.md +++ b/docs/CONTRACTS.md @@ -51,7 +51,9 @@ fighter (saving and buying stay closed). ## 2. HTTP routes Pages (server/worker.ts `page()`): `/` is the intro (intro/index.html); `/?channel=x` is that channel's fighter page; -`/play/` is the channel picker (`/play/?channel=x` moves to `/?channel=x`, `/play` to `/play/`); `/intro/` moves to `/` (301). +`/play/` is the channel picker (`/play/?channel=x` moves to `/?channel=x`, `/play` to `/play/`); `/changelog/` is the release notes +(changelog/index.html, static; its newest release must equal `package.json` `version`, checked by tests/changelog.test.ts); +`/intro/` moves to `/` (301). `/admin/` without `?channel=` sends a signed-in streamer whose own channel is set up (on or turned off) and who can manage it to `/admin/?channel=` (302); anyone else gets the page, which offers sign-in or `/start`. The intro's main buttons say "Back to " when the fighter page @@ -70,7 +72,7 @@ everything else is revalidated. | GET | `/api/session` | optional | none | `{user:{id,login,displayName}\|null, owner, configured, channels:["nesszerra","miolafff"] (the built-ins), productionEnabled:false}`. `productionEnabled` is a constant `false` left from the first rollout, when production stayed closed until the broadcaster was onboarded. Nothing reads it; use `channelState` instead. | 503 when secrets are missing | | GET | `/api/channels` | none | none | `{channels:[login,...], defaultChannel}` (`defaultChannel` is what a URL without `?channel=` falls back to; the overlay uses it): the built-ins first, then the enabled (not paused) registry channels in sign-up order. Cached 60 s per isolate. The picker at `/play/` (the viewer page without `?channel=`) shows "Which stream are you watching?" with one button per channel; every link the site, sign-in and `!fray` hand out carries `?channel=` so a fighter is never saved to the wrong channel by default. Bot lines never put punctuation right after a link (Twitch chat takes it into the link), and the pages drop characters a login can't hold from the end of `?channel=` and show the clean address (`channelParam` in src/channel-param.ts), so `?channel=miolafff,` still opens miolafff | | | GET | `/api/picker` | none | none | `{channels:[{login, top:[{displayName, username, elo, avatar}], catalog?}], catalog}`: the `/play/` picker in one request. Same channels and order as `/api/channels`; `top` is up to three fighters with at least one ranked duel, `catalog` per channel lists only the uploaded looks they wear, and the top-level `catalog` is the built-in list. Cached 60 s per isolate. | | -| GET | `/robots.txt`, `/sitemap.xml` | none | none | robots: `Disallow: /api/` and `/auth/` on the production host, `Disallow: /` on the staging and test hosts. sitemap: production host only, lists `/`, `/play/`, `/start/`; 404 elsewhere | | +| GET | `/robots.txt`, `/sitemap.xml` | none | none | robots: `Disallow: /api/` and `/auth/` on the production host, `Disallow: /` on the staging and test hosts. sitemap: production host only, lists `/`, `/play/`, `/start/`, `/changelog/`; 404 elsewhere | | | POST (GET for export) | `/api/devtools/:channel/profile\|chat\|live\|shop\|export` | dev token (test site only) | as the matching site route; see docs/DEVTOOLS.md | the same room call as the matching site route; `export` returns the raw room tables | 400 `invalid_body` (`text`, `userId`, `username`, `displayName`, `live` or `streamId` of the wrong type), 400 `{error:"Invalid profile fields"}` (a profile field of the wrong type), 401 without the token (an owner session gets 404), 403 channel off | | GET | `/api/health` | none | none | `{ok:true}`; nothing else, so the public answer reveals no version or configuration (the owner's `/api/dev/diagnostics` has `worker.version` and `twitchConfigured`) | 503 `{ok:false}` when secrets are missing | | GET | `/api/access/:channel` | optional | none | `{owner, broadcaster, moderator, canManage, reason}` | 403 for a channel that isn't set up | diff --git a/docs/LIVE_FIX.md b/docs/LIVE_FIX.md index 11157cd..b7eb68c 100644 --- a/docs/LIVE_FIX.md +++ b/docs/LIVE_FIX.md @@ -19,6 +19,14 @@ routes answer `501` with `reason: github_not_configured` or `cloudflare_not_configured`, and the page hides the code editor and release steps. Diagnostics, the error log, live settings work without any of them. +## Versions + +PixFray uses semver below 1.0: a minor bump (`0.8.0` to `0.9.0`) for anything a viewer or streamer can +see, a patch bump for fixes and behind-the-scenes work. `package.json` `version` is the one source. Before a +production deploy from `main`, bump it and add the matching entry at the top of `changelog/index.html` (the same +version in the page's subtitle). `bun run check` fails when the two disagree (tests/changelog.test.ts). A live fix +or hotfix that reaches production gets a patch entry the same way, in its PR. + ## The flow 1. **Save to a branch.** In the code editor, pick a branch named `live-fix/` or diff --git a/docs/PAGES.md b/docs/PAGES.md index f4b0cbe..ffcf9ed 100644 --- a/docs/PAGES.md +++ b/docs/PAGES.md @@ -7,6 +7,7 @@ | `/` | What PixFray is, with a duel playing. A returning viewer gets a "Back to " button. | | `/?channel=` | That channel's fighter page. A signed-in viewer can turn a picture into their own sprite under **Your own sprite** (an optional AI redraw first). It joins their character list once a mod approves it. Fighters are saved per channel: with none saved here, the page offers the look the viewer saved on another channel. Signed out, the page says the fighter shown is a starting one, and edits made before signing in come back after it. When a save reaches the server but the page can't read the answer, it says the profile may have saved and to reload the page to check, instead of "Not saved". Punctuation after the login (`?channel=miolafff,`, as a chat app may add to a link) is dropped. | | `/play/` | Picks the channel. | +| `/changelog/` | What changed in each PixFray version, newest first. The newest entry is the version in `package.json`, which is what production runs. | | `/intro/` | Redirects to `/`. | | `/start/` | Streamer sign-up. | | `/admin/?channel=` | The mod controls. A signed-in streamer who opens `/admin/` lands on their own channel. Viewer sprites waiting for review are at the top of the Characters tab. | diff --git a/index.html b/index.html index 7258705..1950554 100644 --- a/index.html +++ b/index.html @@ -255,7 +255,7 @@

Duel rules

diff --git a/intro/index.html b/intro/index.html index f575bd7..358dc06 100644 --- a/intro/index.html +++ b/intro/index.html @@ -122,7 +122,7 @@

One browser source. Nothing to install.

diff --git a/package.json b/package.json index 07c0c15..4feb5ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mini-chat", - "version": "0.1.0", + "version": "0.8.0", "private": true, "license": "MIT", "type": "module", diff --git a/public/404.html b/public/404.html index 40bafc2..81cb34a 100644 --- a/public/404.html +++ b/public/404.html @@ -24,7 +24,7 @@

This page drifted off the island

diff --git a/public/_headers b/public/_headers index db9f515..b2fa07d 100644 --- a/public/_headers +++ b/public/_headers @@ -24,6 +24,9 @@ /play/* Content-Security-Policy: default-src 'self'; script-src 'self' https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; media-src 'self' data: blob:; object-src 'none'; base-uri 'self'; connect-src 'self' https://cloudflareinsights.com; frame-ancestors 'none' X-Frame-Options: DENY +/changelog/* + Content-Security-Policy: default-src 'self'; script-src 'self' https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; media-src 'self' data: blob:; object-src 'none'; base-uri 'self'; connect-src 'self' https://cloudflareinsights.com; frame-ancestors 'none' + X-Frame-Options: DENY # no-transform keeps Cloudflare from adding its Web Analytics beacon, which OVERLAY_CSP blocks (a console error in OBS # and in the /start demo on every load). max-age=0 and must-revalidate are the assets default, so a new build still loads. diff --git a/public/dashboard.css b/public/dashboard.css index 8af21ab..48c7bdb 100644 --- a/public/dashboard.css +++ b/public/dashboard.css @@ -1383,6 +1383,31 @@ html:has(.config-bar.dirty) { border-left-color: var(--trim); } +/* changelog: releases newest first, separated by rules; the change kind is a quiet badge, never win/loss colors */ +.release + .release { + margin-top: var(--space-6); + padding-top: var(--space-6); + border-top: 1px solid var(--border); +} +.release h2 { + margin: 0; +} +.release .meta { + margin: var(--space-1) 0 var(--space-4); +} +.release ul { + list-style: none; + margin: 0; + padding: 0; + max-width: var(--measure); +} +.release li + li { + margin-top: var(--space-3); +} +.release .badge { + margin-right: var(--space-2); +} + @media (max-width: 900px) { /* narrow screens: the preview card sticks to the top of the screen as a compact bar while you pick in the tabs below it, so the fighter and Save are always in view (dashboard.ts keeps scroll-padding-top clear of it) */ @@ -1470,6 +1495,10 @@ html:has(.config-bar.dirty) { margin: 0; padding: var(--space-1) var(--space-3); } + /* eleven release chips would push the first release off the phone screen */ + .changelog .page-index { + display: none; + } } @media (max-width: 760px) { .topbar-inner { diff --git a/server/hosts.ts b/server/hosts.ts index 457223b..f5fa71d 100644 --- a/server/hosts.ts +++ b/server/hosts.ts @@ -25,18 +25,18 @@ export function siteOrigin(env: HostEnv, url: URL, channel: string): string { export function authOrigin(env: HostEnv, url: URL): string { return hostChannel(env, url) ? url.origin : env.PUBLIC_ORIGIN || url.origin; } -// The home, picker, mod and signup pages (the paths cloudflare.config.ts sends through the Worker; /intro/ only redirects). -export const isPage = (path: string): boolean => /^\/(index\.html)?$|^\/(admin|start|play)(\/|$)/.test(path); -// Page requests on a channel domain: other channels and the site-wide pages (/start, /play, /admin/dev) move to the main +// The home, picker, mod, signup and changelog pages (the paths cloudflare.config.ts sends through the Worker; /intro/ only redirects). +export const isPage = (path: string): boolean => /^\/(index\.html)?$|^\/(admin|start|play|changelog)(\/|$)/.test(path); +// Page requests on a channel domain: other channels and the site-wide pages (/start, /play, /changelog, /admin/dev) move to the main // site; a page without ?channel= gets the domain's channel. 302, so a later change is not stuck in browser caches. export function channelPageRedirect(env: HostEnv, url: URL): Response | null { const channel = hostChannel(env, url), path = url.pathname; if (!channel || !isPage(path)) return null; const asked = (url.searchParams.get("channel") || "").toLowerCase(); - if (env.PUBLIC_ORIGIN && (/^\/(start|play|admin\/dev)(\/|$)/.test(path) || (asked && asked !== channel))) + if (env.PUBLIC_ORIGIN && (/^\/(start|play|changelog|admin\/dev)(\/|$)/.test(path) || (asked && asked !== channel))) return Response.redirect(env.PUBLIC_ORIGIN + path + url.search + url.hash, 302); - if (!asked && !/^\/(start|play|admin\/dev)(\/|$)/.test(path)) { + if (!asked && !/^\/(start|play|changelog|admin\/dev)(\/|$)/.test(path)) { const to = new URL(url); to.searchParams.set("channel", channel); return Response.redirect(to.href, 302); diff --git a/server/worker.ts b/server/worker.ts index d1728f8..244e2c5 100644 --- a/server/worker.ts +++ b/server/worker.ts @@ -313,6 +313,7 @@ async function page(request: Request, env: Env, url: URL): Promise { channel = url.searchParams.get("channel"); if (/^\/intro(\/(index\.html)?)?$/.test(path)) return Response.redirect(url.origin + "/" + url.search, 301); if (path === "/play") return Response.redirect(url.origin + "/play/" + url.search, 301); + if (path === "/changelog") return Response.redirect(url.origin + "/changelog/", 301); if (path === "/play/" && channel) return Response.redirect(url.origin + "/" + url.search, 302); if (path === "/admin/" && !channel) { const own = await ownAdmin(request, env, url); @@ -347,7 +348,7 @@ async function ownAdmin(request: Request, env: Env, url: URL): Promise new Response(body, { diff --git a/start/index.html b/start/index.html index d96959b..f4c91ee 100644 --- a/start/index.html +++ b/start/index.html @@ -88,7 +88,7 @@

Your chat gets its own arena after one sign-in

diff --git a/tests/auth.test.ts b/tests/auth.test.ts index e73afb9..fd4ed3a 100644 --- a/tests/auth.test.ts +++ b/tests/auth.test.ts @@ -532,6 +532,7 @@ test("pages: / is the intro, /?channel= the fighter page, /play/ the picker, and assert.equal(await body("/play/"), "asset /"); assert.equal((await get("/")).headers.get("X-Frame-Options"), "DENY", "the intro gets the page headers too"); assert.equal(await moved("/play", 301), "https://chat.miolaf.xyz/play/"); + assert.equal(await moved("/changelog", 301), "https://chat.miolaf.xyz/changelog/"); assert.equal(await moved("/play/?channel=miolafff", 302), "https://chat.miolaf.xyz/?channel=miolafff"); for (const old of ["/intro", "/intro/", "/intro/index.html"]) assert.equal(await moved(old, 301), "https://chat.miolaf.xyz/"); diff --git a/tests/changelog.test.ts b/tests/changelog.test.ts new file mode 100644 index 0000000..37e8e82 --- /dev/null +++ b/tests/changelog.test.ts @@ -0,0 +1,55 @@ +// The changelog page (changelog/index.html) against package.json: the newest release is the version that ships. +// Run with: node --import ./tests/register.ts --test tests/changelog.test.ts +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { URL as NodeURL } from "node:url"; + +const read = (file: string) => readFileSync(new NodeURL("../" + file, import.meta.url), "utf8"); +// Comments are dropped so the format note at the top of the page isn't read as a release. +const html = read("changelog/index.html").replace(//g, ""); +// as: package.json is ours and always has a version string +const { version } = JSON.parse(read("package.json")) as { version: string }; + +/** One release section as the page writes it: its anchor, the version in its heading and its date. */ +type Release = { id: string; version: string; date: string; items: number }; +const releases: Release[] = [...html.matchAll(/
/g)].map( + ([block, id]) => ({ + id: id ?? "", + version: /]*>([^<]+)<\/span>/.exec(block)?.[1] ?? "", + date: /