Skip to content

Commit fd2e186

Browse files
Merge pull request #52 from Jamesllllllllll/codex/settings-extension-ui-copy
Harden Twitch panel beta flows and refine playlist UX
2 parents e814ae8 + d3e507f commit fd2e186

21 files changed

+1305
-513
lines changed

AGENTS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
## UI Copy
2+
3+
- Treat normal app UI as end-user UI.
4+
- Do not place development, build, deployment, testing, or setup instructions in normal user-facing screens.
5+
- Keep commands, file paths, environment variables, route names, build artifacts, and implementation details in documentation or explicit developer-only views.
6+
- Write UI copy around what the user can do in the product right now.
7+
- Write UI copy in present tense.
8+
- If a beta or testing action is intentionally exposed in the UI, keep the copy short and action-oriented without internal implementation details.

docs/twitch-panel-extension-beta-rollout-checklist.md

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -81,39 +81,47 @@ In the Twitch Extensions console, use the final production app origin for the pa
8181

8282
Keep `Chat Capabilities` disabled unless the panel sends extension chat messages.
8383

84-
## 8. Move the panel from Local Test to Hosted Test
85-
86-
Build the hosted panel files:
87-
88-
```bash
89-
npm run build:extension:panel
90-
```
91-
92-
Upload the contents of:
93-
94-
```text
95-
dist/twitch-extension/panel
84+
## 8. Move the panel from Local Test to Hosted Test
85+
86+
Build the hosted panel files:
87+
88+
```bash
89+
npm run build:extension:panel
90+
```
91+
92+
When you build the Hosted Test artifact, make sure the shell sets the final app origin:
93+
94+
```bash
95+
VITE_TWITCH_EXTENSION_API_BASE_URL=https://your-app-host npm run build:extension:panel
96+
```
97+
98+
Upload the contents of:
99+
100+
```text
101+
dist/twitch-extension/panel
96102
```
97103

98104
Set the Hosted Test asset paths for the panel version:
99105

100106
- `Panel Viewer Path`: `index.html`
101107
- `Config Path`: `index.html`
102108
- `Live Config Path`: leave blank unless you add a live config surface
103-
104-
Use Hosted Test for beta channels that should load the production panel without your local dev server or tunnel.
105-
106-
## 9. Set tester access
109+
110+
Use Hosted Test for beta channels that should load the production panel without your local dev server or tunnel.
111+
112+
The uploaded Twitch panel bundle is separate from the website deploy. When you change panel code or the extension API base URL, rebuild the panel artifact and upload a new zip before you retest Hosted Test.
113+
114+
## 9. Set tester access
107115

108116
Add beta channels to:
109117

110118
- `Testing Account Allowlist`
111119

112120
If the version stays unreleased, only accounts on the testing allowlist can install and use the panel in testing mode.
113121

114-
## 10. Verify the beta flows
115-
116-
Verify these paths on the Hosted Test build:
122+
## 10. Verify the beta flows
123+
124+
Verify these paths on the Hosted Test build:
117125

118126
- viewer, unlinked:
119127
- playlist loads
@@ -127,8 +135,10 @@ Verify these paths on the Hosted Test build:
127135
- channel owner:
128136
- playlist moderation controls appear
129137
- set current, mark played, delete item, and request-kind changes work
130-
- channel moderator:
131-
- playlist moderation controls follow the channel's moderator capability settings
138+
- channel moderator:
139+
- playlist moderation controls follow the channel's moderator capability settings
140+
141+
If an extension request fails in production, check the frontend Worker logs. Slow and failed panel `bootstrap` and `state` requests log a trace id, elapsed time, and stage timings for channel lookup, viewer resolution, viewer request state, and live playlist state.
132142

133143
## 11. Keep the website login expectation clear
134144

docs/twitch-panel-extension-local-test.md

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,22 @@ Run the app:
3232
npm run dev
3333
```
3434

35-
Build the standalone panel artifact:
36-
37-
```bash
38-
npm run build:extension:panel
39-
```
40-
41-
The built panel artifact is written to:
42-
43-
```text
44-
dist/twitch-extension/panel
35+
Build the standalone panel artifact:
36+
37+
```bash
38+
npm run build:extension:panel
39+
```
40+
41+
For any build that should call a deployed app origin, set the API base URL in the same shell before you build:
42+
43+
```bash
44+
VITE_TWITCH_EXTENSION_API_BASE_URL=https://your-app-host npm run build:extension:panel
45+
```
46+
47+
The built panel artifact is written to:
48+
49+
```text
50+
dist/twitch-extension/panel
4551
```
4652

4753
## Local workflow
@@ -73,8 +79,9 @@ Opening the website from the panel does not create a website session.
7379

7480
The website recognizes the viewer only when the browser already has the normal RockList.Live session cookie. Otherwise the viewer still signs in through the website Twitch OAuth flow.
7581

76-
## After Local Test
77-
78-
- package the final hosted panel artifact for Twitch Hosted Test
79-
- add review-prep notes for fetched URLs and enabled capabilities
80-
- validate the full identity-share flow with a real extension registration
82+
## After Local Test
83+
84+
- package the final hosted panel artifact for Twitch Hosted Test
85+
- rebuild and re-upload the Hosted Test zip whenever panel code or `VITE_TWITCH_EXTENSION_API_BASE_URL` changes
86+
- add review-prep notes for fetched URLs and enabled capabilities
87+
- validate the full identity-share flow with a real extension registration

src/components/channel-rules-panel.tsx

Lines changed: 10 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,6 @@ type CharterMatch = {
1717
trackCount: number;
1818
};
1919

20-
type SongMatch = {
21-
songId: number;
22-
songTitle: string;
23-
artistId?: number | null;
24-
artistName?: string | null;
25-
};
26-
2720
type SongGroupMatch = {
2821
groupedProjectId: number;
2922
songTitle: string;
@@ -36,7 +29,6 @@ type SearchResponse = {
3629
artists?: ArtistMatch[];
3730
charters?: CharterMatch[];
3831
songs?: SongGroupMatch[];
39-
songVersions?: SongMatch[];
4032
};
4133

4234
export function ChannelRulesPanel(props: {
@@ -65,20 +57,16 @@ export function ChannelRulesPanel(props: {
6557
const [artistQuery, setArtistQuery] = useState("");
6658
const [charterQuery, setCharterQuery] = useState("");
6759
const [songGroupQuery, setSongGroupQuery] = useState("");
68-
const [songVersionQuery, setSongVersionQuery] = useState("");
6960
const [setlistQuery, setSetlistQuery] = useState("");
7061
const [debouncedArtistQuery, setDebouncedArtistQuery] = useState("");
7162
const [debouncedCharterQuery, setDebouncedCharterQuery] = useState("");
7263
const [debouncedSongGroupQuery, setDebouncedSongGroupQuery] = useState("");
73-
const [debouncedSongVersionQuery, setDebouncedSongVersionQuery] =
74-
useState("");
7564
const [debouncedSetlistQuery, setDebouncedSetlistQuery] = useState("");
7665
const [artistMatches, setArtistMatches] = useState<ArtistMatch[]>([]);
7766
const [charterMatches, setCharterMatches] = useState<CharterMatch[]>([]);
7867
const [songGroupMatches, setSongGroupMatches] = useState<SongGroupMatch[]>(
7968
[]
8069
);
81-
const [songVersionMatches, setSongVersionMatches] = useState<SongMatch[]>([]);
8270
const [setlistMatches, setSetlistMatches] = useState<ArtistMatch[]>([]);
8371
const [searchError, setSearchError] = useState<string | null>(null);
8472

@@ -87,18 +75,11 @@ export function ChannelRulesPanel(props: {
8775
setDebouncedArtistQuery(artistQuery.trim());
8876
setDebouncedCharterQuery(charterQuery.trim());
8977
setDebouncedSongGroupQuery(songGroupQuery.trim());
90-
setDebouncedSongVersionQuery(songVersionQuery.trim());
9178
setDebouncedSetlistQuery(setlistQuery.trim());
9279
}, 350);
9380

9481
return () => window.clearTimeout(timeout);
95-
}, [
96-
artistQuery,
97-
charterQuery,
98-
setlistQuery,
99-
songGroupQuery,
100-
songVersionQuery,
101-
]);
82+
}, [artistQuery, charterQuery, setlistQuery, songGroupQuery]);
10283

10384
const mutateRules = useMutation({
10485
mutationFn: async (body: Record<string, unknown>) => {
@@ -181,9 +162,6 @@ export function ChannelRulesPanel(props: {
181162
void runSearch("song", debouncedSongGroupQuery, (payload) => {
182163
setSongGroupMatches(payload.songs ?? []);
183164
});
184-
void runSearch("song-version", debouncedSongVersionQuery, (payload) => {
185-
setSongVersionMatches(payload.songVersions ?? []);
186-
});
187165
}
188166

189167
if (props.canManageSetlist) {
@@ -200,7 +178,6 @@ export function ChannelRulesPanel(props: {
200178
debouncedCharterQuery,
201179
debouncedSetlistQuery,
202180
debouncedSongGroupQuery,
203-
debouncedSongVersionQuery,
204181
props.canManageBlacklist,
205182
props.canManageSetlist,
206183
props.slug,
@@ -214,10 +191,6 @@ export function ChannelRulesPanel(props: {
214191
() => new Set(props.charters.map((item) => item.charterId)),
215192
[props.charters]
216193
);
217-
const blacklistedSongIds = useMemo(
218-
() => new Set(props.songs.map((item) => item.songId)),
219-
[props.songs]
220-
);
221194
const blacklistedSongGroupIds = useMemo(
222195
() => new Set(props.songGroups.map((item) => item.groupedProjectId)),
223196
[props.songGroups]
@@ -236,9 +209,6 @@ export function ChannelRulesPanel(props: {
236209
const visibleSongGroupMatches = songGroupMatches.filter(
237210
(song) => !blacklistedSongGroupIds.has(song.groupedProjectId)
238211
);
239-
const visibleSongVersionMatches = songVersionMatches.filter(
240-
(song) => !blacklistedSongIds.has(song.songId)
241-
);
242212
const visibleSetlistMatches = setlistMatches.filter(
243213
(artist) => !setlistArtistIds.has(artist.artistId)
244214
);
@@ -392,24 +362,10 @@ export function ChannelRulesPanel(props: {
392362

393363
<SearchManageCard
394364
title="Blacklisted versions"
395-
inputValue={songVersionQuery}
396-
onInputChange={setSongVersionQuery}
397-
placeholder="Search versions by title"
398-
matches={visibleSongVersionMatches.map((song) => ({
399-
key: `song-version-match-${song.songId}`,
400-
label: song.artistName
401-
? `${song.songTitle} - ${song.artistName}`
402-
: song.songTitle,
403-
meta: `Version ID ${song.songId}`,
404-
onAdd: () =>
405-
mutateRules.mutate({
406-
action: "addBlacklistedSong",
407-
songId: song.songId,
408-
songTitle: song.songTitle,
409-
artistId: song.artistId ?? null,
410-
artistName: song.artistName ?? undefined,
411-
}),
412-
}))}
365+
inputValue=""
366+
onInputChange={() => {}}
367+
placeholder=""
368+
matches={[]}
413369
currentItems={props.songs.map((item) => ({
414370
key: `song-version-current-${item.songId}`,
415371
label: item.artistName
@@ -425,6 +381,7 @@ export function ChannelRulesPanel(props: {
425381
isPending={mutateRules.isPending}
426382
emptyCurrentLabel="No blacklisted versions."
427383
canManage={props.canManageBlacklist}
384+
showSearch={false}
428385
hideReadOnlyRemoveAction
429386
/>
430387
</>
@@ -494,10 +451,12 @@ function SearchManageCard(props: {
494451
isPending: boolean;
495452
emptyCurrentLabel: string;
496453
canManage?: boolean;
454+
showSearch?: boolean;
497455
hideReadOnlyRemoveAction?: boolean;
498456
}) {
499457
const normalizedLength = props.inputValue.trim().length;
500458
const canManage = props.canManage !== false;
459+
const showSearch = props.showSearch !== false;
501460
const showRemoveAction = canManage || !props.hideReadOnlyRemoveAction;
502461

503462
return (
@@ -506,7 +465,7 @@ function SearchManageCard(props: {
506465
<CardTitle>{props.title}</CardTitle>
507466
</CardHeader>
508467
<CardContent className="grid gap-4 px-5 pb-5 pt-0">
509-
{canManage ? (
468+
{canManage && showSearch ? (
510469
<>
511470
<Input
512471
value={props.inputValue}
@@ -520,7 +479,7 @@ function SearchManageCard(props: {
520479
) : null}
521480
</>
522481
) : null}
523-
{canManage && normalizedLength >= 2 ? (
482+
{canManage && showSearch && normalizedLength >= 2 ? (
524483
<div className="grid gap-3">
525484
{props.matches.length > 0 ? (
526485
props.matches.map((match) => (

0 commit comments

Comments
 (0)