feat: integrate Stories widget with backend endpoint#2172
feat: integrate Stories widget with backend endpoint#2172stackingsaunter wants to merge 2 commits intomasterfrom
Conversation
Add stories endpoint plumbing for HTTP and Wails, wire the Home Stories card to fetch from /api/alby/stories, and keep it first in the right column. Made-with: Cursor
📝 WalkthroughWalkthroughA new Stories feature fetches story data from the Alby internal API and surfaces it via a backend API route and frontend widget. It adds service and model APIs, a React StoriesWidget with localStorage persistence and YouTube embed support, and updates CSP/frame-src for YouTube. Changes
Sequence DiagramsequenceDiagram
participant User
participant Frontend as Frontend:\nStoriesWidget
participant HTTP as HTTP_Handler
participant API as API_Layer
participant Service as AlbyOAuthService
participant Alby as Alby_API
User->>Frontend: mount
Frontend->>HTTP: GET /api/alby/stories
HTTP->>API: GetStories(ctx)
API->>Service: GetStories(ctx)
Service->>Alby: HTTP GET /stories (10s timeout)
Alby-->>Service: JSON []Story
Service-->>API: []Story
API-->>HTTP: []Story
HTTP-->>Frontend: 200 OK + stories
Frontend->>Frontend: read localStorage (viewed IDs)
User->>Frontend: click story
Frontend->>Frontend: mark viewed, normalize YouTube URL, open modal
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (3)
frontend/src/components/home/widgets/StoriesWidget.tsx (2)
229-238: Consider restricting iframe to YouTube-only URLs.
getYouTubeEmbedUrlfalls back to the original URL when parsing fails or the URL isn't recognized as YouTube. IfvideoUrlcontains a non-YouTube URL, it will be embedded directly. Since the story data comes from the backend, verify that the CSPframe-srcdirective appropriately restricts allowed iframe sources.💡 Alternative: Only render iframe for valid YouTube URLs
+function isYouTubeUrl(url: string): boolean { + try { + const host = new URL(url).hostname.replace(/^www\./, ""); + return host === "youtu.be" || host.endsWith("youtube.com"); + } catch { + return false; + } +} - {activeStory.videoUrl && ( + {activeStory.videoUrl && isYouTubeUrl(activeStory.videoUrl) && ( <div className="aspect-video w-full bg-black">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/home/widgets/StoriesWidget.tsx` around lines 229 - 238, The iframe currently renders any activeStory.videoUrl because getYouTubeEmbedUrl falls back to the original URL; update StoriesWidget to only render the iframe when the URL is recognized as YouTube by using the output of getYouTubeEmbedUrl (or a new isYouTubeUrl helper) and verify embedUrl is non-empty before creating the iframe; reference activeStory.videoUrl and getYouTubeEmbedUrl (or a new isYouTubeUrl) and ensure the component skips rendering the iframe for non-YouTube URLs so only validated YouTube embeds are used.
107-135: Use SWR and the typedrequest()helper for data fetching.The coding guidelines specify using SWR for server state management and the typed
request()helper for HTTP requests. This would provide automatic caching, revalidation, and type safety.♻️ Suggested refactor using SWR
+import useSWR from "swr"; +import { request } from "src/utils/request"; + +type StoryResponse = { + id: number; + title: string; + avatar: string; + videoUrl?: string; +}; export function StoriesWidget() { - const [stories, setStories] = React.useState<Story[]>([]); - const [isLoading, setIsLoading] = React.useState(true); + const { data, isLoading } = useSWR<StoryResponse[]>( + "/api/alby/stories", + request + ); + + const stories: Story[] = React.useMemo( + () => + (data ?? []).map((story) => ({ + id: String(story.id), + title: story.title, + avatar: story.avatar, + videoUrl: story.videoUrl, + })), + [data] + ); const [activeStory, setActiveStory] = React.useState<Story | null>(null); const [viewedIds, setViewedIds] = React.useState<Set<string>>(loadViewedStoryIds); - - React.useEffect(() => { - const loadStories = async () => { - try { - const response = await fetch("/api/alby/stories"); - if (!response.ok) { - throw new Error(`Failed to fetch stories: ${response.status}`); - } - const payload = (await response.json()) as Array<{ - id: number; - title: string; - avatar: string; - videoUrl?: string; - }>; - const mappedStories = payload.map((story) => ({ - id: String(story.id), - title: story.title, - avatar: story.avatar, - videoUrl: story.videoUrl, - })); - setStories(mappedStories); - } catch { - setStories([]); - } finally { - setIsLoading(false); - } - }; - - void loadStories(); - }, []);As per coding guidelines: "Use SWR for server state management" and "Use the typed request() helper in frontend/src/utils/request.ts for HTTP requests".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/home/widgets/StoriesWidget.tsx` around lines 107 - 135, Replace the manual fetch inside the useEffect/loadStories with SWR and the typed request() helper: import useSWR and request, call useSWR('/api/alby/stories', () => request<Array<{id:number;title:string;avatar:string;videoUrl?:string}>>('/api/alby/stories')), derive isLoading from the SWR status instead of setIsLoading, map the returned payload (convert id to string and preserve title/avatar/videoUrl) and call setStories or initialise stories from swr.data, and remove the loadStories async function and its try/catch/finally block; ensure types match and handle the empty or error case using SWR's error value.alby/alby_oauth_service.go (1)
1374-1411: Wrap returned errors with context inGetStories.Several returns propagate raw errors, which makes upstream debugging harder. Prefer contextual wrapping in this method.
♻️ Suggested change
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { logger.Logger.WithError(err).Error("Error creating request to stories endpoint") - return nil, err + return nil, fmt.Errorf("create stories request: %w", err) } @@ body, err := io.ReadAll(res.Body) if err != nil { logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": url, }).Error("Failed to read response body") - return nil, errors.New("failed to read response body") + return nil, fmt.Errorf("read stories response body: %w", err) } @@ if err := json.Unmarshal(body, &stories); err != nil { logger.Logger.WithFields(logrus.Fields{ "body": string(body), "error": err, }).Error("Failed to decode stories API response") - return nil, err + return nil, fmt.Errorf("decode stories response: %w", err) }As per coding guidelines,
**/*.go: Use error wrapping withfmt.Errorf("context: %w", err)for debugging.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@alby/alby_oauth_service.go` around lines 1374 - 1411, GetStories currently returns raw errors from http.NewRequestWithContext, client.Do, io.ReadAll and json.Unmarshal which loses context; update each return to wrap the underlying error with fmt.Errorf including a short contextual prefix (e.g. fmt.Errorf("GetStories: creating request: %w", err), fmt.Errorf("GetStories: performing request: %w", err), fmt.Errorf("GetStories: reading response body: %w", err), fmt.Errorf("GetStories: decoding stories response: %w", err)). Also replace the plain errors.New("failed to read response body") with a wrapped error that includes the original read error, and keep the non-success status error message descriptive (include status code and body) using fmt.Errorf.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@alby/alby_oauth_service.go`:
- Around line 1374-1411: GetStories currently returns raw errors from
http.NewRequestWithContext, client.Do, io.ReadAll and json.Unmarshal which loses
context; update each return to wrap the underlying error with fmt.Errorf
including a short contextual prefix (e.g. fmt.Errorf("GetStories: creating
request: %w", err), fmt.Errorf("GetStories: performing request: %w", err),
fmt.Errorf("GetStories: reading response body: %w", err),
fmt.Errorf("GetStories: decoding stories response: %w", err)). Also replace the
plain errors.New("failed to read response body") with a wrapped error that
includes the original read error, and keep the non-success status error message
descriptive (include status code and body) using fmt.Errorf.
In `@frontend/src/components/home/widgets/StoriesWidget.tsx`:
- Around line 229-238: The iframe currently renders any activeStory.videoUrl
because getYouTubeEmbedUrl falls back to the original URL; update StoriesWidget
to only render the iframe when the URL is recognized as YouTube by using the
output of getYouTubeEmbedUrl (or a new isYouTubeUrl helper) and verify embedUrl
is non-empty before creating the iframe; reference activeStory.videoUrl and
getYouTubeEmbedUrl (or a new isYouTubeUrl) and ensure the component skips
rendering the iframe for non-YouTube URLs so only validated YouTube embeds are
used.
- Around line 107-135: Replace the manual fetch inside the useEffect/loadStories
with SWR and the typed request() helper: import useSWR and request, call
useSWR('/api/alby/stories', () =>
request<Array<{id:number;title:string;avatar:string;videoUrl?:string}>>('/api/alby/stories')),
derive isLoading from the SWR status instead of setIsLoading, map the returned
payload (convert id to string and preserve title/avatar/videoUrl) and call
setStories or initialise stories from swr.data, and remove the loadStories async
function and its try/catch/finally block; ensure types match and handle the
empty or error case using SWR's error value.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3b5a8727-71ed-4ecf-a346-d0d0eee3a766
📒 Files selected for processing (11)
alby/alby_oauth_service.goalby/models.goapi/api.goapi/models.gofrontend/src/components/home/widgets/StoriesWidget.tsxfrontend/src/screens/Home.tsxfrontend/vite.config.tshttp/alby_http_service.gohttp/http_service.gotests/mocks/AlbyOAuthService.gowails/wails_handlers.go
- Add contextual actions in the story dialog (update hub with version, open Alby Go in-app, install extension) keyed by kind or title - Use preview stories when the stories API request fails - Pass hub version from useInfo into the update link Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
frontend/src/components/home/widgets/StoriesWidget.tsx (1)
341-348: Use proper router navigation for internal links.The internal route
/appstore/alby-gouses a raw<a>tag which causes a full page reload instead of client-side navigation. Consider using React Router'sLinkcomponent for SPA navigation.♻️ Suggested fix
+import { Link } from "react-router-dom"; return ( - <a + <Link - href={action.url} + to={action.url} className="inline-flex h-9 items-center rounded-md border border-white/15 px-4 text-sm font-medium text-white transition-colors hover:bg-white/10" > {action.label} - </a> + </Link> );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/home/widgets/StoriesWidget.tsx` around lines 341 - 348, In StoriesWidget replace the raw anchor used for internal navigation: detect when action.url is an internal path (e.g. startsWith('/')) and render React Router's Link instead of the <a> so navigation is client-side; preserve the existing className, children (action.label) and other props, and import Link from 'react-router-dom'; keep using a normal <a> for external URLs (and add target="_blank" rel="noopener noreferrer" if appropriate) to avoid breaking external behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/components/home/widgets/StoriesWidget.tsx`:
- Around line 189-218: The component currently uses manual fetch inside
loadStories and local useEffect/useState (setStories, isLoading) — replace that
with SWR and the typed request() helper: remove loadStories and effect, call
useSWR('/api/alby/stories', () =>
request<Array<{id:number;title:string;avatar:string;videoUrl?:string;kind?:string}>>('/api/alby/stories'))
in StoriesWidget, map the returned data to the existing story shape (id as
string, title, avatar, videoUrl, kind) before passing to setStories or directly
render from the mapped data, use fallbackData or onError to fall back to
previewStories, and derive loading state from SWR (e.g., !data && !error)
instead of setIsLoading.
---
Nitpick comments:
In `@frontend/src/components/home/widgets/StoriesWidget.tsx`:
- Around line 341-348: In StoriesWidget replace the raw anchor used for internal
navigation: detect when action.url is an internal path (e.g. startsWith('/'))
and render React Router's Link instead of the <a> so navigation is client-side;
preserve the existing className, children (action.label) and other props, and
import Link from 'react-router-dom'; keep using a normal <a> for external URLs
(and add target="_blank" rel="noopener noreferrer" if appropriate) to avoid
breaking external behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 87ef3fc3-53be-42b1-b0e8-e9593b94b1d2
📒 Files selected for processing (1)
frontend/src/components/home/widgets/StoriesWidget.tsx
| React.useEffect(() => { | ||
| const loadStories = async () => { | ||
| try { | ||
| const response = await fetch("/api/alby/stories"); | ||
| if (!response.ok) { | ||
| throw new Error(`Failed to fetch stories: ${response.status}`); | ||
| } | ||
| const payload = (await response.json()) as Array<{ | ||
| id: number; | ||
| title: string; | ||
| avatar: string; | ||
| videoUrl?: string; | ||
| }>; | ||
| const mappedStories = payload.map((story) => ({ | ||
| id: String(story.id), | ||
| title: story.title, | ||
| avatar: story.avatar, | ||
| videoUrl: story.videoUrl, | ||
| kind: (story as { kind?: Story["kind"] }).kind, | ||
| })); | ||
| setStories(mappedStories); | ||
| } catch { | ||
| setStories(previewStories); | ||
| } finally { | ||
| setIsLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| void loadStories(); | ||
| }, []); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Use SWR and the typed request() helper per coding guidelines.
The current implementation uses raw fetch() and manual useState/useEffect for server state. Per project coding guidelines, this should use SWR for server state management and the typed request() helper from frontend/src/utils/request.ts for HTTP requests.
Using SWR provides built-in caching, automatic revalidation, request deduplication, and consistent loading/error patterns across the codebase.
♻️ Suggested refactor using SWR
+import useSWR from "swr";
+import { swrFetcher } from "src/utils/swr";
+type StoryResponse = {
+ id: number;
+ title: string;
+ avatar: string;
+ videoUrl?: string;
+ kind?: Story["kind"];
+};
export function StoriesWidget() {
- const [stories, setStories] = React.useState<Story[]>([]);
- const [isLoading, setIsLoading] = React.useState(true);
const [activeStory, setActiveStory] = React.useState<Story | null>(null);
const [viewedIds, setViewedIds] =
React.useState<Set<string>>(loadViewedStoryIds);
const { data: info } = useInfo();
- React.useEffect(() => {
- const loadStories = async () => {
- try {
- const response = await fetch("/api/alby/stories");
- if (!response.ok) {
- throw new Error(`Failed to fetch stories: ${response.status}`);
- }
- const payload = (await response.json()) as Array<{
- id: number;
- title: string;
- avatar: string;
- videoUrl?: string;
- }>;
- const mappedStories = payload.map((story) => ({
- id: String(story.id),
- title: story.title,
- avatar: story.avatar,
- videoUrl: story.videoUrl,
- kind: (story as { kind?: Story["kind"] }).kind,
- }));
- setStories(mappedStories);
- } catch {
- setStories(previewStories);
- } finally {
- setIsLoading(false);
- }
- };
-
- void loadStories();
- }, []);
+ const { data: storiesData, isLoading } = useSWR<StoryResponse[]>(
+ "/api/alby/stories",
+ swrFetcher,
+ { onError: () => {} } // silently handle errors, we fall back below
+ );
+
+ const stories = React.useMemo(() => {
+ if (!storiesData) {
+ return isLoading ? [] : previewStories;
+ }
+ return storiesData.map((story) => ({
+ id: String(story.id),
+ title: story.title,
+ avatar: story.avatar,
+ videoUrl: story.videoUrl,
+ kind: story.kind,
+ }));
+ }, [storiesData, isLoading]);As per coding guidelines: "Use SWR for server state management" and "Use the typed request() helper in frontend/src/utils/request.ts for HTTP requests".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/home/widgets/StoriesWidget.tsx` around lines 189 -
218, The component currently uses manual fetch inside loadStories and local
useEffect/useState (setStories, isLoading) — replace that with SWR and the typed
request() helper: remove loadStories and effect, call
useSWR('/api/alby/stories', () =>
request<Array<{id:number;title:string;avatar:string;videoUrl?:string;kind?:string}>>('/api/alby/stories'))
in StoriesWidget, map the returned data to the existing story shape (id as
string, title, avatar, videoUrl, kind) before passing to setStories or directly
render from the mapped data, use fallbackData or onError to fall back to
previewStories, and derive loading state from SWR (e.g., !data && !error)
instead of setIsLoading.
Summary
/api/alby/stories) for HTTP and Wails.StoriesWidgetUI and fetch stories from the backend endpoint.text-xs,leading-normal, and no NEW badge.kindfield (update|alby-go|alby-extension); if missing, kind is inferred from the title. Actions: Update Alby Hub → getalby.com update URL with current Hub version (useInfo); Open Alby Go → in-app/appstore/alby-go; Install Alby Extension → getalby.com (new tab).Data & playback
/api/alby/stories.videoUrlis expected to be a normal watch/embeddable URL; the dialog uses the existing YouTube iframe helper and CSP entries for YouTube.Test plan
go test ./alby ./api ./http ./wailscd frontend && yarn tsc:compileMade with Cursor
Summary by CodeRabbit
New Features
Chores