Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Changelog

Code, tooling and operations changes, newest first. What players and streamers notice is on the site's `/changelog/`
page (`changelog/index.html`). Both use the version in `package.json`.

Rules (checked by `tests/changelog.test.ts`):

- The newest version here equals `package.json` `version`, which is what production runs.
- Every release on `/changelog/` has a heading here with the same date. A release with nothing a player or streamer
would notice appears only here.
- Versions are semver below 1.0: a minor bump for new things players or streamers can see, a patch bump for fixes
and behind-the-scenes work.
0.1.0 to 0.7.1 were cut after the fact from git history.

## 0.8.1 - 2026-10-20

- The changelog splits in two: `/changelog/` keeps only what players and streamers notice, and this file holds code
notes for every release. `tests/changelog.test.ts` checks both against `package.json` (#45).

## 0.8.0 - 2026-10-20

- Channel point backend: the `channel.channel_points_custom_reward_redemption.add` EventSub subscription,
`createPointReward` and `deletePointReward` admin actions (broadcaster only), idempotent grants and refunds keyed
by redemption ID, and a per-room redemption log in `server/points.ts` (#43).
- Debug tracing: alarm, duel and chat logs, daily room counters, and `scripts/logs.ts` to read them (#42).
- `/changelog/` page, with `package.json` `version` as the version source (#44).
- `tests/ui.ts` mock config gains `pairRankedPerStream`, missing since #40.

## 0.7.1 - 2026-10-16

- The room tick dropped expired rematch locks without marking the state changed, so the alarm re-armed itself about 8
times a second (164k alarms in 7 days). Expired locks are now saved (#41).

## 0.7.0 - 2026-10-15

- Upgrade cost model with level costs, stat ceilings and presets in `server/upgrades.ts` (#34, #35, #36).
- The sprite subject finder and background removal run in a Web Worker (#37, #38).
- The bot e2e runs on staging again, room routes are awaited, and shop answers are parsed before use (#39).
- `pairRankedPerStream` config and the live-only ranked check (#40).

## 0.6.2 - 2026-10-12

- Strict TypeScript port of the server, pages, overlay modules, tests, scripts and build config (#11, #16 to #24, #27,
#28, #31 to #33). `bun run port-check` proves a port PR changes no runtime code.
- Requests, outside answers, stored data and what the browser receives are parsed at the boundary (#25, #29, #30).
- The owner editor refuses config saves (#9, #13); the deploy check job runs without the Cloudflare token (#14);
rollbacks deploy only from the base branch (#15); `DEV_TOOLS_TOKEN` lives only in the test secrets (#8); pinned
`@types/three` (#10); stale tests removed (#12).

## 0.6.1 - 2026-10-09

- Changes land by pull request with review (#1); dependency audit and a `sharp` pin (#2).
- Rank history per channel and an owner-only `restoreRank` action (#4 to #6).
- Daily backups of every channel into D1, kept 90 days, with point-in-time restore (#7).
- oxfmt, oxlint and tsc in `bun run check`; CI on every push with gitleaks; pre-commit hooks.

## 0.6.0 - 2026-10-07

- Open-source prep: everything reads `site.config.js`, secrets moved to `~/.pixfray`, gitleaks config, Bun for
scripts, AGENTS.md and DESIGN.md.
- Hardening: HSTS and a script CSP, durable EventSub dedupe, the public health check says only ok.
- Overlay modules versioned per deploy (`?v=` hash), hashed bundles cached for a year, `/api/picker`.
- The live chat e2e follows the bot account.

## 0.5.0 - 2026-10-06

- Self-hosting from one `site.config.js` (owner, channels, Workers, domains, bot).
- The owner page releases from a local checkout.
- `priceSchema` 2 migration for the shop price cut.
- Channel export through the dev token.

## 0.4.0 - 2026-10-05

- Main site moved to pixfray.xyz and staging to staging.pixfray.xyz.
- The bot reads chat through EventSub and replies through Helix, with 429 retry; a staging debug bot.
- Baked scroll stills for the redesign, and browser tests on the GPU.

## 0.3.0 - 2026-10-04

- Live Twitch chat e2e loop (`bun run test:live`).
- Edge rate limit of 20 requests per 10 seconds.
- Saved-look lookups retry once.

## 0.2.0 - 2026-10-02

- Workers Logs with query strings redacted.
- Security headers and socket caps.
- A re-runnable build script for the CC0 characters.
- Overlay settings come from the server, and overlays reload on deploy.

## 0.1.0 - 2026-10-01

- One Cloudflare Worker with a SQLite Durable Object per channel.
- Twitch EventSub webhooks replace the local chat relay; StreamElements as a second command source.
- Test-site dev tools, command logging, and an overlay clock skew fix.
29 changes: 9 additions & 20 deletions changelog/index.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>What's new in PixFray · Changelog</title>
<meta name="description" content="Every PixFray release, newest first: new duel features, fighter upgrades, bot commands and fixes.">
<meta name="description" content="What changed in PixFray for players and streamers, newest first: new duel features, fighter upgrades, bot commands and fixes.">
<meta name="theme-color" content="#17110c">
<link rel="icon" href="/favicon.svg" type="image/svg+xml"><link rel="icon" href="/favicon.ico" sizes="16x16 32x32 48x48"><link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="preload" href="/assets/fonts/fraunces-latin.woff2" as="font" type="font/woff2" crossorigin>
Expand All @@ -12,21 +12,21 @@
<nav aria-label="Pages"><a href="/">See a duel</a><a href="/start/">For streamers</a><a class="nav-cta" href="/play/">Pick your fighter</a></nav>
</div></header>

<!-- Releases go newest first. Each one is a <section class="release" id="v<version>"> with an h2 holding the version
and name and a <time datetime>; the newest version equals package.json "version" (tests/changelog.test.ts). -->
<!-- Only what players and streamers notice; code changes go in CHANGELOG.md. Releases go newest first. Each one is a
<section class="release" id="v<version>"> with an h2 holding the version and name and a <time datetime> that
matches its CHANGELOG.md heading. #current-version equals package.json "version" (tests/changelog.test.ts). -->
<main class="page">
<header class="page-header">
<h1>What's new in PixFray</h1>
<p class="subtitle">Every release, newest first. PixFray is on version <span class="num" id="current-version">0.8.0</span>.</p>
<p class="subtitle">What changed for players and streamers, newest first. PixFray is on version <span class="num" id="current-version">0.8.1</span>. Code changes are listed in <code>CHANGELOG.md</code> in the PixFray source.</p>
</header>

<div class="with-index changelog">
<nav class="page-index" aria-label="Releases"><ol>
<li><a href="#v0.8.0">0.8.0 Channel point rewards</a></li>
<li><a href="#v0.7.1">0.7.1 Steadier rooms</a></li>
<li><a href="#v0.7.0">0.7.0 Deeper fighter stats</a></li>
<li><a href="#v0.6.2">0.6.2 Under the hood</a></li>
<li><a href="#v0.6.1">0.6.1 Ranks and backups</a></li>
<li><a href="#v0.6.2">0.6.2 Small fixes</a></li>
<li><a href="#v0.6.1">0.6.1 Rank fixes</a></li>
<li><a href="#v0.6.0">0.6.0 Viewer sprites and bot controls</a></li>
<li><a href="#v0.5.0">0.5.0 Open sign-up and the PixFray bot</a></li>
<li><a href="#v0.4.0">0.4.0 PixFray</a></li>
Expand All @@ -43,17 +43,9 @@ <h2 id="v0.8.0-title"><span class="num">0.8.0</span> Channel point rewards</h2>
<li><span class="badge">New</span> 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.</li>
<li><span class="badge">New</span> A viewer who redeems without a saved fighter gets their points back. A repeated delivery from Twitch never pays twice.</li>
<li><span class="badge">New</span> This changelog page.</li>
<li><span class="badge">Under the hood</span> Duel, alarm and chat logs and daily room counters, so problems are easier to trace.</li>
</ul>
</section>

<section class="release" id="v0.7.1" aria-labelledby="v0.7.1-title">
<h2 id="v0.7.1-title"><span class="num">0.7.1</span> Steadier rooms</h2>
<p class="meta"><time datetime="2026-10-16">16 October 2026</time></p>
<ul>
<li><span class="badge">Fixed</span> An expired rematch no longer keeps a channel's room waking up over and over.</li>
</ul>
</section>

<section class="release" id="v0.7.0" aria-labelledby="v0.7.0-title">
<h2 id="v0.7.0-title"><span class="num">0.7.0</span> Deeper fighter stats</h2>
Expand All @@ -69,21 +61,19 @@ <h2 id="v0.7.0-title"><span class="num">0.7.0</span> Deeper fighter stats</h2>
</section>

<section class="release" id="v0.6.2" aria-labelledby="v0.6.2-title">
<h2 id="v0.6.2-title"><span class="num">0.6.2</span> Under the hood</h2>
<h2 id="v0.6.2-title"><span class="num">0.6.2</span> Small fixes</h2>
<p class="meta"><time datetime="2026-10-12">12 October 2026</time></p>
<ul>
<li><span class="badge">Under the hood</span> All of PixFray now runs on strict TypeScript, and everything it receives is checked before use.</li>
<li><span class="badge">Fixed</span> A stray empty slot after full build slots, and the mod page on a channel without an arena.</li>
</ul>
</section>

<section class="release" id="v0.6.1" aria-labelledby="v0.6.1-title">
<h2 id="v0.6.1-title"><span class="num">0.6.1</span> Ranks and backups</h2>
<h2 id="v0.6.1-title"><span class="num">0.6.1</span> Rank fixes</h2>
<p class="meta"><time datetime="2026-10-09">9 October 2026</time></p>
<ul>
<li><span class="badge">Fixed</span> Saving your fighter keeps your rank.</li>
<li><span class="badge">Fixed</span> The <code>!fray</code> link opens your channel instead of the channel picker.</li>
<li><span class="badge">New</span> Every channel is backed up daily and kept for 90 days, and a lost rank can be restored.</li>
</ul>
</section>

Expand Down Expand Up @@ -111,7 +101,6 @@ <h2 id="v0.5.0-title"><span class="num">0.5.0</span> Open sign-up and the PixFra
<li><span class="badge">New</span> The PixFray bot answers duel commands in chat, so streamers need no other chat bot.</li>
<li><span class="badge">New</span> Search the channel picker for any channel running PixFray.</li>
<li><span class="badge">New</span> Mods can try <code>!checkin</code> for 15 minutes while offline.</li>
<li><span class="badge">New</span> Anyone can run their own copy of PixFray from one config file.</li>
<li><span class="badge">Changed</span> Shop prices cut to about a third.</li>
</ul>
</section>
Expand Down
3 changes: 2 additions & 1 deletion docs/CONTRACTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ fighter (saving and buying stay closed).

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/`); `/changelog/` is the release notes
(changelog/index.html, static; its newest release must equal `package.json` `version`, checked by tests/changelog.test.ts);
(changelog/index.html, static, game changes only; its subtitle names `package.json` `version`, and each release on it has
a `CHANGELOG.md` heading with the same date, whose newest equals `package.json`, 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=<their login>` (302); anyone else
Expand Down
8 changes: 5 additions & 3 deletions docs/LIVE_FIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ settings work without any of them.

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.
production deploy from `main`, bump it, add an entry at the top of `CHANGELOG.md` (code changes) and set the
version in the subtitle of `changelog/index.html`. When players or streamers would notice the release, also add an
entry at the top of the page, with the same version and date; a code-only release stays off the page.
`bun run check` fails when these 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

Expand Down
2 changes: 1 addition & 1 deletion docs/PAGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
| `/` | What PixFray is, with a duel playing. A returning viewer gets a "Back to <channel>" button. |
| `/?channel=<login>` | 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. |
| `/changelog/` | What changed for players and streamers in each PixFray version, newest first. The subtitle names the version in `package.json`, which is what production runs; code-only releases are in `CHANGELOG.md`. |
| `/intro/` | Redirects to `/`. |
| `/start/` | Streamer sign-up. |
| `/admin/?channel=<login>` | 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. |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mini-chat",
"version": "0.8.0",
"version": "0.8.1",
"private": true,
"license": "MIT",
"type": "module",
Expand Down
26 changes: 23 additions & 3 deletions tests/changelog.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// The changelog page (changelog/index.html) against package.json: the newest release is the version that ships.
// The game changelog page (changelog/index.html) and the code changelog (CHANGELOG.md) against package.json.
// Run with: node --import ./tests/register.ts --test tests/changelog.test.ts
import test from "node:test";
import assert from "node:assert/strict";
Expand All @@ -21,16 +21,21 @@ const releases: Release[] = [...html.matchAll(/<section class="release" id="([^"
items: (block.match(/<li>/g) || []).length,
}),
);
/** The code changelog's `## <version> - <date>` headings, newest first. */
const md = [...read("CHANGELOG.md").matchAll(/^## (\S+) - (\S+)$/gm)].map(([, v, d]) => ({
version: v ?? "",
date: d ?? "",
}));
const semver = (v: string) => v.split(".").map(Number);
const newer = (a: string, b: string) => {
const [x, y] = [semver(a), semver(b)];
for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
return false;
};

test("the newest release on the changelog is package.json's version", () => {
test("CHANGELOG.md's newest release is package.json's version, and the page names it", () => {
assert.ok(releases.length > 0, "the page lists releases");
assert.equal(releases[0]?.version, version);
assert.equal(md[0]?.version, version);
assert.match(html, new RegExp('<span class="num" id="current-version">' + version.replace(/\./g, "\\.") + "</span>"));
});

Expand All @@ -53,3 +58,18 @@ test("releases go newest first: versions strictly down, dates never up", () => {
assert.ok(a.date >= b.date, a.version + " is not older than " + b.version);
}
});

test("CHANGELOG.md goes newest first with semver versions and real dates", () => {
for (const [i, r] of md.entries()) {
assert.match(r.version, /^\d+\.\d+\.\d+$/, r.version);
assert.ok(!Number.isNaN(Date.parse(r.date)) && /^\d{4}-\d{2}-\d{2}$/.test(r.date), r.version + " has a date");
const prev = md[i - 1];
if (prev) assert.ok(newer(prev.version, r.version) && prev.date >= r.date, prev.version + " before " + r.version);
}
});

test("every release on the page is in CHANGELOG.md with the same date", () => {
for (const r of releases) {
assert.equal(md.find((m) => m.version === r.version)?.date, r.date, r.version);
}
});
Loading