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
2 changes: 1 addition & 1 deletion .env.deploy.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ TWITCH_EVENTSUB_SECRET=
SESSION_SECRET=
ADMIN_TWITCH_USER_IDS=
TWITCH_BOT_USERNAME=requestbot
TWITCH_SCOPES=openid user:read:moderated_channels channel:bot
TWITCH_SCOPES=openid user:read:moderated_channels moderator:read:chatters channel:bot
9 changes: 6 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ TWITCH_EVENTSUB_SECRET=local-dev-eventsub-secret
SESSION_SECRET=local-dev-session-secret
# Twitch user IDs for initial admins - comma separated, no quotation marks, like this: 1234567,2345678
ADMIN_TWITCH_USER_IDS=
# The username of the shared bot account
# The username of the shared bot account.
# For local development, this should usually be your dedicated test bot account.
# Production should keep its own production bot username in deployed env/secrets.
TWITCH_BOT_USERNAME=requestbot
# Scopes for the shared bot account
TWITCH_SCOPES=openid user:read:moderated_channels channel:bot
# Broadcaster OAuth scopes used by the main app login.
# These should include the channel permissions needed for bot replies and chatter lookups.
TWITCH_SCOPES=openid user:read:moderated_channels moderator:read:chatters channel:bot
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,35 @@ All notable changes to this project will be documented in this file.
## [Unreleased]

### Added
- Shared bot reconnect controls for admins, including the ability to replace the connected bot account safely.
- VIP token management from both chat commands and the dashboard, with Twitch user lookup, chatter-aware search, and an editable token table in the app.
- Public played-history search.
- Charter blacklisting, including exact charter matching and clearer handling when only some song versions are blocked.
- Richer sample catalog metadata for artists, charters, tunings, and future filtering work.
- Production-ready Sentry scaffolding for the Cloudflare app and backend workers, with DSN-based opt-in for local development and Cloudflare-managed secrets for deployed environments.
- GitHub issue templates, a pull request template, and a repository `CODE_OF_CONDUCT.md`.

### Changed
- Bot replies now use Twitch's bot-badge-compatible reply path, and the app prompts broadcasters to reconnect Twitch if required permissions are missing.
- Broadcaster login now requests the Twitch permissions needed for chatter-aware moderation and bot-badged replies.
- The app header and settings pages now surface Twitch reauthorization more clearly when a reconnect is required.
- Local-development guidance now strongly separates production bot/broadcaster usage from local testing and explains the risks of cross-environment chat handling.
- The moderation dashboard now supports faster Twitch username search with debouncing, in-chat prioritization, and clearer saved-state feedback for VIP tokens.
- Search results now show newer song versions first, and public search includes a dedicated `!edit` copy command.
- Public playlist, dashboard playlist, search, and home-page experiences have been refined for mobile screens and easier browsing.
- Blacklist and setlist management now use exact IDs instead of loose text matching, improving moderation accuracy.
- Public search now behaves more like a browsable catalog and shows clearer demo-database guidance.
- Simplified catalog song source URLs to always derive the Ignition download link from the song source ID instead of storing `source_url` in the database.
- Added a migration to remove the redundant `catalog_songs.source_url` column and updated the sample catalog seed to match the new schema.
- Tightened schema version checks so the app only accepts migrations that are actually present in the repo.
- Expanded deployment and environment documentation for Sentry configuration in local development and production.

### Fixed
- Duplicate EventSub deliveries for `!addvip` no longer grant multiple VIP tokens or queue duplicate bot replies.
- Twitch reply handling now distinguishes between accepted API requests and messages that Twitch actually sent to chat.
- Bot/account status screens now show the real connected bot identity instead of only the configured bot name.
- Production deployment config regeneration now stays in sync after remote migrations.

## [0.1.0] - 2026-03-18

### Added
Expand Down
10 changes: 9 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,19 @@
```bash
npm install
npm run db:bootstrap:local
npm run lint
npm run typecheck
npm run test
npm run format
npm run lint
npm run build
```

If you changed user-facing flows or browser interactions, also run:

```bash
npm run test:e2e
```

4. Open a pull request.
5. Wait for CI to pass.
6. Review the preview deployment if one is enabled for the repository.
Expand Down Expand Up @@ -42,6 +49,7 @@ The app checks the latest applied migration at runtime and fails early if the lo

- Keep changes focused.
- Include tests when you change behavior.
- Run `npm run format` before `npm run lint`. This repo has been seeing avoidable AI-generated formatting drift, and Biome lint is much cleaner after formatting first.
- If a change affects Twitch auth, EventSub, playlist mutations, or migrations, call that out in the PR description.
- If a change affects deployment or Cloudflare bindings, update the docs in the same PR.

Expand Down
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ TWITCH_CLIENT_SECRET=
TWITCH_EVENTSUB_SECRET=local-dev-eventsub-secret
SESSION_SECRET=local-dev-session-secret
TWITCH_BOT_USERNAME=requestbot
TWITCH_SCOPES=openid user:read:moderated_channels channel:bot
TWITCH_SCOPES=openid user:read:moderated_channels moderator:read:chatters channel:bot
ADMIN_TWITCH_USER_IDS=
VITE_ALLOWED_HOSTS=
```
Expand All @@ -95,7 +95,7 @@ For basic local development, set:
- `TWITCH_EVENTSUB_SECRET`
- `SESSION_SECRET`
- `TWITCH_BOT_USERNAME`
- `TWITCH_SCOPES=openid user:read:moderated_channels channel:bot`
- `TWITCH_SCOPES=openid user:read:moderated_channels moderator:read:chatters channel:bot`
- `VITE_ALLOWED_HOSTS=` if you need extra Vite hostnames

To test Twitch sign-in, bot behavior, and EventSub locally, also set:
Expand All @@ -106,6 +106,8 @@ To test Twitch sign-in, bot behavior, and EventSub locally, also set:

`ADMIN_TWITCH_USER_IDS` should contain the Twitch user ID for the admin account that is allowed to connect the shared bot account and access admin pages.

If a broadcaster connected before `channel:bot` was added to your configured scopes, they need to reconnect Twitch from the app before bot replies can use Twitch's bot badge path.

Sentry stays off locally unless you explicitly set a DSN:

- `SENTRY_DSN`
Expand Down Expand Up @@ -165,6 +167,26 @@ http://localhost:9000

The repo auto-runs local D1 migrations before `dev`, `test`, and `build`.

### Verification before commit

Run checks in this order:

```bash
npm run typecheck
npm run test
npm run format
npm run lint
npm run build
```

If you changed browser-driven flows, also run:

```bash
npm run test:e2e
```

Formatting before lint is intentional. Biome lint is much cleaner after `npm run format`, and this avoids a lot of AI-generated formatting churn before commit.

### 6. Twitch application setup for local auth

If you want Twitch login to work locally, your Twitch developer application must include both redirect URIs:
Expand Down Expand Up @@ -475,7 +497,7 @@ gh secret set CLOUDFLARE_D1_DATABASE_ID
gh secret set CLOUDFLARE_SESSION_KV_ID
gh secret set APP_URL --body "https://your-production-url.example"
gh variable set TWITCH_BOT_USERNAME --body "your_bot_username"
gh variable set TWITCH_SCOPES --body "openid user:read:moderated_channels channel:bot"
gh variable set TWITCH_SCOPES --body "openid user:read:moderated_channels moderator:read:chatters channel:bot"
```

For the full deploy and GitHub workflow details, use [docs/deployment-workflow.md](/docs/deployment-workflow.md).
Expand Down
26 changes: 25 additions & 1 deletion docs/bot-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,13 @@ TWITCH_EVENTSUB_SECRET=...
SESSION_SECRET=...
ADMIN_TWITCH_USER_IDS=your_main_twitch_user_id
TWITCH_BOT_USERNAME=Pants_Bot_
TWITCH_SCOPES=openid user:read:moderated_channels channel:bot
TWITCH_SCOPES=openid user:read:moderated_channels moderator:read:chatters channel:bot
```

For local development, `TWITCH_BOT_USERNAME` should usually be your dedicated test bot account. Production should keep its own bot username in deployed env or secrets. The app enforces that the connected bot login matches `TWITCH_BOT_USERNAME`, so changing bot accounts locally requires changing local `.env` first.

`TWITCH_SCOPES` belongs to the broadcaster login flow, not the bot login flow. It should include `channel:bot` so chat replies can use Twitch's bot badge path. If a broadcaster connected before `channel:bot` was present, they need to reconnect Twitch.

2. Make sure your Twitch developer application has both redirect URIs registered:

- `${APP_URL}/auth/twitch/callback`
Expand Down Expand Up @@ -70,6 +74,26 @@ npm run db:migrate

- Replies are sent with the bot account's user token, not the broadcaster token.
- If Twitch returns `401` while sending chat, the backend refreshes the bot token once and retries automatically.
- Testing against the same broadcaster in both production and local/tunnel environments can cause both environments to receive and process the same chat command.

### Local vs production warning

If a streamer or moderator tests bot commands on a channel that is connected in the live app while a local tunnel/dev environment is also connected for that same broadcaster, cross-environment behavior can become confusing.

There are two different cases:

- same broadcaster + same bot account:
local and production compete for the same `channel.chat.message` subscription, so one environment can effectively take over chat handling from the other
- same broadcaster + different bot accounts:
Twitch can allow both subscriptions, so both environments can receive commands from that same channel and both may reply

For safe bot testing:

- use a dedicated test broadcaster/channel whenever possible
- use a dedicated test bot account for local development
- set local `.env` `TWITCH_BOT_USERNAME` to that test bot account before reconnecting the bot
- do not sign a production broadcaster into the local environment
- avoid keeping both production and local EventSub subscriptions active for the same broadcaster

### Current limits

Expand Down
82 changes: 74 additions & 8 deletions docs/local-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,50 @@ npm run dev

To exercise Twitch login, EventSub, and bot replies, fill in the Twitch-related values in `.env`.

For local bot testing, set `TWITCH_BOT_USERNAME` in `.env` to your dedicated test bot account, not the production bot account. The bot OAuth callback only accepts the username configured in local env, so if you want to connect `jimmy_test_bot_` locally, your local `.env` must also say `TWITCH_BOT_USERNAME=jimmy_test_bot_`.

Keep the production bot username only in production secrets or deployed env. Do not point local development at the production bot account unless you intentionally want local testing to use the live bot identity.

`TWITCH_SCOPES` applies to the broadcaster's main app login, not the shared bot login. It needs `channel:bot` so bot replies can use Twitch's bot badge path, and it needs `moderator:read:chatters` for the chatter-first VIP lookup flow.

### Important local testing warning

Do not test bot commands against a channel that is also connected in the live app unless you intentionally want both environments to react.

There are two different failure modes:

#### Same broadcaster + same bot account in local and production

This does not usually create duplicate chat handling.

Instead, it creates a subscription ownership conflict. Twitch treats `channel.chat.message` subscriptions as unique by event type plus condition, and the condition includes both the broadcaster ID and bot user ID. If local and production both try to use the same broadcaster with the same bot account, one environment can end up owning the subscription and the other can fail or appear to stop receiving chat events.

That means local testing can still interfere with production, even if both environments do not reply at the same time.

#### Same broadcaster + different bot accounts in local and production

This is the more dangerous case for duplicate behavior.

Because the bot user ID is different, Twitch can allow both subscriptions at once. Then a single chat command in that broadcaster's channel can be seen by both environments:

- once by production
- once by local development

That can cause:

- duplicate bot replies in chat
- duplicated side effects if both environments act on the same command
- confusing logs where both environments appear to handle the same message

Recommended practice:

- use a separate test broadcaster/channel for local bot testing
- use a separate test bot account for local bot testing
- set local `.env` `TWITCH_BOT_USERNAME` to the test bot account username
- do not connect a production broadcaster to local development
- keep only one active EventSub webhook subscription for a given broadcaster when you are debugging command behavior
- do not leave a local tunnel subscription active while also testing the same channel in production

### Public HTTPS for local Twitch testing

`localhost` is enough for basic Twitch OAuth testing, but full local testing for this app works better with a public HTTPS URL because Twitch webhooks need a reachable callback target.
Expand Down Expand Up @@ -144,6 +188,8 @@ Then update:
- `https://dev.example.com/auth/twitch/callback`
- `https://dev.example.com/auth/twitch/bot/callback`

Before testing chat commands through the tunnel, make sure the same broadcaster is not still actively subscribed to the production EventSub callback unless that is intentional.

#### ngrok

As an alternative:
Expand All @@ -159,32 +205,52 @@ Use the generated HTTPS URL for:

If the ngrok URL changes, update both `.env` and the Twitch app redirect URIs.

### Daily commands
### Verification before commit

Run checks in this order:

Lint:
1. Typecheck:

```bash
npm run lint
npm run typecheck
```

Typecheck:
2. Tests:

```bash
npm run typecheck
npm run test
```

Tests:
3. Format:

```bash
npm run test
npm run format
```

4. Lint:

```bash
npm run lint
```

Production build:
5. Production build:

```bash
npm run build
```

If you changed browser-driven behavior or UI flows, also run:

```bash
npm run test:e2e
```

Why `format` before `lint`:

- Biome lint is much less noisy after formatting first
- AI-generated edits often introduce avoidable formatting drift
- running `npm run format` first catches a large class of pre-commit issues cheaply

### Cloudflare deploy inputs

`npm run deploy` and `npm run db:bootstrap:remote` read deployment values from `.env.deploy`, not `.env`.
Expand Down
2 changes: 1 addition & 1 deletion src/components/dashboard-page-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function DashboardPageHeader(props: DashboardPageHeaderProps) {
</div>

{props.aside ? (
<div className="dashboard-page-header__aside grid shrink-0 gap-3 md:justify-items-end">
<div className="dashboard-page-header__aside grid w-full min-w-0 gap-3 md:w-auto md:max-w-sm md:justify-items-end">
{props.aside}
</div>
) : null}
Expand Down
Loading
Loading