Conversation
Vite + React frontend, Express backend, and an end-to-end Twitch OAuth Authorization Code flow (login, callback, session cookie, /auth/me, logout) verified against a throwaway page. Sessions are in-memory for now, replaced by Postgres in the next phase.
Merge dev branch with docs
…irect URI The fallback default was still 3001 (the rejected first attempt from Day 2, see c-docs/NOTES.md), so a missing .env would silently reintroduce the redirect_mismatch error against Twitch's registered callback URL.
Users and templates tables provisioned on Neon per the PLAN.md data model. OAuth callback now upserts a real users row instead of holding profile data only in memory, and /api/templates gains full owner-scoped CRUD behind the existing session cookie. Verified end to end through a real authenticated browser session.
The day-by-day narrative belongs in c-docs/NOTES.md, which already has the full history; the README should only tell users what works.
The embed's ONLINE/OFFLINE events are unreliable (a long-standing Twitch platform issue), so the frontend polls GET /api/stream-status?logins=... instead. The endpoint batches up to 100 logins into one Helix /streams call using a cached client-credentials app access token. No auth required, matching anonymous read-only viewing.
- Grid reducer implements the PLAN.md GridState shape; panels stay flat siblings so switching audio focus never remounts a Twitch iframe. - VideoPanel mounts one Twitch Embed per channel and controls mute and volume via the player API; the offline overlay reads the Helix-polled channel.online flag. - Selection and Both/All audio modes, with the removal-fallback edge case promoting another channel. - ChatBar keeps every chat iframe mounted for the whole session and switches tabs via CSS visibility only; the chat tab follows the active audio channel until manually overridden. - Native per-panel actions: follow (login-gated), donate link, channel link, remove. - Twitch login wired into the app shell, with a login prompt modal for gated actions. - Visual design pass in hand-written CSS: color and typography tokens, light and dark themes, hover and focus states, transitions, inline SVG icon set.
visibility is overridable by descendants: .chat-panel.visible set visibility: visible and so re-revealed itself inside the hidden .chat-bar-body, leaving the active chat's right edge showing through the collapsed 48px rail. Hide panels explicitly while collapsed.
…resh - OAuth now requests the user:read:follows scope (was empty); existing users re-login once to grant it. - twitchUserToken.js implements refresh-on-expiry for the stored per-user token pair (rotated refresh tokens persisted); a failed refresh raises ReloginRequiredError, which routes map to a cleared session and 401. - GET /api/followed-streams (auth) wraps Helix /streams/followed with the user's own token; 401/403 from Twitch (pre-scope token or revoked access) forces the same clean re-login. - GET /api/resolve-channels (public, app token) reports which logins exist, for template-load validation. - Helix 429s now pass through as 429 instead of 502. Nothing from Twitch is persisted; responses are passed straight through.
- TemplateManager (toolbar dropdown): save the current grid as a named template, list/load/delete, explicit saving/saved/error state, save blocked below 2 channels, everything login-gated with an inline login prompt when logged out. - FollowedLive (header dropdown, logged-in only): live channels the user follows via /api/followed-streams, with one-click add-to-grid, disabled for channels already in the grid or at the 5-channel cap. - Template load validates logins via /api/resolve-channels first; unresolvable channels render a dashed "channel not found" panel with no embed and only a remove action, and are skipped when picking the active audio channel (SET_STATE reducer action). - Session-expiry (401) on any authed call resets the client to logged-out. - Dev-only window.__streamhive dispatch hook for browser automation.
New order: brand, Templates, add-channel form, Online now, account, Audio, log out/in. Templates now opens near the left edge of the bar, so its dropdown panel anchors left instead of the default right:0 (which would have pushed it off-screen); Online now keeps the default anchor since it still sits close to the right edge.
New right-side order: Online now, Audio, Account. The account block (avatar+name+logout) becomes a dropdown trigger, matching the existing Templates/Online-now pattern, so logout no longer sits as a separate top-level button. Also adds a collapse toggle for the whole top bar, mirroring the chat bar's open/close affordance to reclaim vertical space for the grid. No iframes live in the top bar, so collapsing just unmounts the controls instead of needing the chat bar's visibility-only hide.
Drop the redundant "AUDIO" eyebrow label above the Selection/Both-All segmented control — the two button labels already say what they do. Swap the Online now trigger's heart icon for the existing red live-dot indicator (already used elsewhere for live status), since a heart reads as "favorites" rather than "currently live".
auto-fit with minmax(320px, 1fr) packed as many columns as would fit, so 4 channels ended up as one wide row and 5 as an unbalanced 4+1 — "division only horizontal" for the counts where it matters most. Explicit per-count templates fix this: 2 and 3 stay a single row, 4 becomes 2x2, 5 becomes 3-over-2. Verified live for all four counts via getBoundingClientRect (2 rows confirmed for 4 and 5). Narrow screens (<=900px) fold 3 and 5 down to 2 columns.
Account menu becomes a proper vertical list: Twitch account (external
link to the user's profile), Settings, a divider, then Log out — instead
of a single standalone logout button.
Settings holds two decided scopes: default audio mode / chat-bar-open
state for new sessions (both were hardcoded in the reducer's initial
state until now), and a manual light/dark theme override alongside the
existing System (prefers-color-scheme) default. All three are stored in
localStorage only, no backend involved.
Theme override works via a data-theme attribute on <html>: index.css
gets :root[data-theme="light"/"dark"] blocks after the prefers-color-
scheme query, so they win once set; removing the attribute (System) hands
control back to the OS setting. main.jsx applies any stored override
before the first paint to avoid a flash of the wrong theme.
Verified live: setTheme('dark'/'light'/'system') each resolved --bg to
the correct value immediately, and 'system' correctly cleared the
attribute and fell back to the media query. Not verified: the actual
Settings modal UI and Twitch-account link, since both only render inside
AccountMenu, which needs a real login.
- Drop the accent-colored outline ring on the header avatar; it made a 24px Twitch profile picture read as a boxed icon rather than a photo. - Collapsed top bar now hides the "StreamHive" text too, keeping only the logo mark and the toggle, with tighter padding — maximizes grid space instead of just hiding the controls. - Empty-state headline replaced: "Watch up to 5 streams at once" undersold the app; "One screen. Every stream that matters." aims higher without claiming anything unbuilt.
Swap the solid-fill btn-primary for a new btn-quiet variant: same transparent/bordered shape as the other top-bar buttons, accent color on the icon/text only. Still reads as the form's action without outweighing everything around it. Scoped to this one button — Save in TemplateManager keeps btn-primary, wasn't part of the complaint.
Contributor
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Merges dev into main, taking the project from scaffold to a working end-to-end Twitch multi-stream viewer with OAuth, templates, and “Online now” discovery.
Changes:
- Added a React/Vite frontend with multi-channel grid playback, chat bar, templates, and settings (theme/audio/chat defaults).
- Added an Express/Postgres backend supporting Twitch OAuth, token refresh, Helix polling endpoints, and template CRUD.
- Updated repository docs and ignore files for local dev setup and secrets hygiene.
Reviewed changes
Copilot reviewed 29 out of 37 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/vite.config.js | Adds Vite config enabling React plugin. |
| frontend/src/twitchEmbed.js | Adds Twitch embed script loader with singleton promise. |
| frontend/src/settings.js | Adds localStorage-backed user settings + theme application. |
| frontend/src/main.jsx | Boots app and applies stored theme before first paint. |
| frontend/src/index.css | Adds global theme tokens, base styles, and shared button variants. |
| frontend/src/gridReducer.js | Introduces grid reducer/state for channels, audio, volumes, chat behavior. |
| frontend/src/components/icons.jsx | Adds app icon set + logo mark components. |
| frontend/src/components/VideoPanel.jsx | Implements Twitch embed panel with actions, overlays, and audio control. |
| frontend/src/components/VideoGrid.jsx | Implements symmetric and focus-based grid layout logic. |
| frontend/src/components/TemplateManager.jsx | Adds login-gated template save/load/delete UI. |
| frontend/src/components/SettingsModal.jsx | Adds settings modal for defaults + theme override. |
| frontend/src/components/FollowedLive.jsx | Adds “Online now” dropdown with followed + trending live streams. |
| frontend/src/components/ChatBar.jsx | Adds always-mounted multi-tab chat sidebar. |
| frontend/src/components/AudioModeControl.jsx | Adds segmented control for audio mode selection. |
| frontend/src/components/AddChannelForm.jsx | Adds channel add UI w/ max channel cap behavior. |
| frontend/src/components/AccountMenu.jsx | Adds account dropdown (Twitch link, settings, logout). |
| frontend/src/assets/vite.svg | Adds Vite asset. |
| frontend/src/assets/react.svg | Adds React asset. |
| frontend/src/App.jsx | Adds main app shell wiring reducer, polling, auth, templates, UI composition. |
| frontend/src/App.css | Adds full app styles for topbar, grid, chat, dropdowns, modals. |
| frontend/public/icons.svg | Adds SVG symbol sprite sheet used by UI. |
| frontend/public/favicon.svg | Adds favicon asset. |
| frontend/package.json | Defines frontend scripts and dependencies. |
| frontend/index.html | Adds Vite HTML entrypoint. |
| frontend/README.md | Adds frontend scaffold README. |
| frontend/.oxlintrc.json | Adds Oxlint configuration. |
| frontend/.gitignore | Adds frontend ignores (dist, node_modules, editor files). |
| backend/src/twitchUserToken.js | Adds user-token refresh + persistence logic. |
| backend/src/twitchAppToken.js | Adds cached app-token fetcher for Helix calls. |
| backend/src/index.js | Adds Express server with OAuth flow, Helix endpoints, and template CRUD. |
| backend/src/db.js | Adds pg Pool initialization. |
| backend/package.json | Defines backend scripts and dependencies. |
| README.md | Updates root README with current status and setup instructions. |
| .gitignore | Adds root ignores for secrets, deps, build outputs, editor files. |
Files not reviewed (2)
- backend/package-lock.json: Generated file
- frontend/package-lock.json: Generated file
Comments suppressed due to low confidence (1)
frontend/src/twitchEmbed.js:1
scriptPromiseis never reset on load failure, so a single transient network error will “poison” future calls (they’ll keep returning the same rejected promise and never retry). ResetscriptPromisetonullin theonerrorhandler and reject with a realError(the current rejection value is an event, which is hard to diagnose upstream).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| // Match the Twitch chat iframe theme to the app theme once at load; the | ||
| // param only takes effect at iframe creation anyway. | ||
| const dark = window.matchMedia('(prefers-color-scheme: dark)').matches |
Comment on lines
+39
to
+48
| <div className="chat-tabs" role="tablist" aria-label="Channel chats"> | ||
| {channels.map((channel) => ( | ||
| <button | ||
| key={channel.id} | ||
| type="button" | ||
| role="tab" | ||
| aria-selected={channel.id === activeChatChannelId} | ||
| className={`chat-tab${channel.id === activeChatChannelId ? ' active' : ''}`} | ||
| onClick={() => onSelectChat(channel.id)} | ||
| > |
Comment on lines
+36
to
+38
| <div className="modal-backdrop" onClick={onClose}> | ||
| <div className="modal settings-modal" role="dialog" aria-modal="true" onClick={(e) => e.stopPropagation()}> | ||
| <h2>Settings</h2> |
Comment on lines
+213
to
+214
| <div className="modal" role="dialog" aria-modal="true" onClick={(e) => e.stopPropagation()}> | ||
| <p>{loginPrompt}</p> |
Comment on lines
+54
to
+57
| async function pollStreamStatus() { | ||
| const res = await fetch(`${BACKEND_URL}/api/stream-status?logins=${logins}`) | ||
| if (!res.ok) return | ||
| const { online } = await res.json() |
| import { MAX_CHANNELS } from '../gridReducer' | ||
| import { IconChevronDown, IconPlus } from './icons' | ||
|
|
||
| const BACKEND_URL = 'http://localhost:3000' |
Comment on lines
+63
to
+64
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [channel.id, channel.loginName]) |
Comment on lines
+145
to
+156
| app.get('/auth/me', requireAuth, async (req, res) => { | ||
| const { rows } = await pool.query( | ||
| 'SELECT login, display_name, avatar_url FROM users WHERE id = $1', | ||
| [req.userId], | ||
| ); | ||
| const user = rows[0]; | ||
| res.json({ | ||
| login: user.login, | ||
| displayName: user.display_name, | ||
| avatarUrl: user.avatar_url, | ||
| }); | ||
| }); |
Comment on lines
+26
to
+27
| const sessions = new Map(); | ||
| const pendingStates = new Set(); |
Comment on lines
+137
to
+141
| res.cookie('session_id', sessionId, { | ||
| httpOnly: true, | ||
| sameSite: 'lax', | ||
| maxAge: 1000 * 60 * 60 * 24 * 7, | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Merge from
devintomain. Brings the app from an empty scaffold to an end-to-end working product against real Twitchchannels.
Phase 0 — scaffold:
Phase 1 — viewing experience:
Phase 2 — account features:
Type of change
feat— new featureChecklist