Skip to content

Commit 01a4d0c

Browse files
devakoneclaude
andcommitted
fix: enable share image generation for repo analysis pages
ShareActions was passing jobId as the userId path param, causing 404s. Now passes userId separately and appends ?jobId= to API URLs. The OG share route supports jobId by fetching from vibe_insights and analysis_jobs instead of user_profiles. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6193cea commit 01a4d0c

4 files changed

Lines changed: 76 additions & 16 deletions

File tree

apps/web/src/app/analysis/[jobId]/AnalysisClient.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,6 +846,7 @@ export default function AnalysisClient({ jobId }: { jobId: string }) {
846846
shareHeadline={shareTemplate.headline}
847847
shareTemplate={shareImageTemplate}
848848
entityId={jobId}
849+
userId={data?.userId ?? undefined}
849850
storyEndpoint={storyEndpoint}
850851
/>
851852
</div>

apps/web/src/app/api/share/[format]/[userId]/route.tsx

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export async function GET(
1111
) {
1212
try {
1313
const { format, userId } = await params;
14-
14+
const jobId = new URL(request.url).searchParams.get("jobId");
15+
1516
if (!["og", "square", "story"].includes(format)) {
1617
return new Response("Invalid format", { status: 400 });
1718
}
@@ -60,21 +61,73 @@ export async function GET(
6061

6162
const supabase = createClient(supabaseUrl, supabaseKey);
6263

63-
// Fetch Profile
64-
const { data: profile, error } = await supabase
65-
.from("user_profiles")
66-
.select("persona_id, persona_name, persona_tagline, persona_confidence, total_repos, total_commits, axes_json, narrative_json")
67-
.eq("user_id", userId)
68-
.maybeSingle();
64+
// Fetch data: job-specific or unified profile
65+
let profile: {
66+
persona_id: string;
67+
persona_name: string | null;
68+
persona_tagline: string | null;
69+
persona_confidence: string | null;
70+
total_repos: number | null;
71+
total_commits: number | null;
72+
axes_json: Record<string, { score: number }> | null;
73+
narrative_json: { insight?: string; summary?: string } | null;
74+
} | null = null;
75+
76+
if (jobId) {
77+
// Job-specific share: fetch from vibe_insights + analysis_jobs
78+
const [vibeResult, jobResult, insightsResult] = await Promise.all([
79+
supabase
80+
.from("vibe_insights")
81+
.select("persona_id, persona_name, persona_tagline, persona_confidence, axes_json")
82+
.eq("job_id", jobId)
83+
.maybeSingle(),
84+
supabase
85+
.from("analysis_jobs")
86+
.select("commit_count")
87+
.eq("id", jobId)
88+
.eq("user_id", userId)
89+
.maybeSingle(),
90+
supabase
91+
.from("analysis_insights")
92+
.select("persona_label, persona_confidence, persona_id, narrative_json")
93+
.eq("job_id", jobId)
94+
.maybeSingle(),
95+
]);
6996

70-
if (error) {
71-
console.error("Supabase error:", error);
72-
throw new Error(`Profile fetch failed: ${error.message}`);
97+
const vibe = vibeResult.data;
98+
const job = jobResult.data;
99+
const insights = insightsResult.data;
100+
101+
if (vibe || insights) {
102+
profile = {
103+
persona_id: vibe?.persona_id ?? insights?.persona_id ?? "balanced_builder",
104+
persona_name: vibe?.persona_name ?? insights?.persona_label ?? null,
105+
persona_tagline: vibe?.persona_tagline ?? null,
106+
persona_confidence: vibe?.persona_confidence ?? insights?.persona_confidence ?? null,
107+
total_repos: 1,
108+
total_commits: job?.commit_count ?? null,
109+
axes_json: vibe?.axes_json ?? null,
110+
narrative_json: insights?.narrative_json ?? null,
111+
};
112+
}
113+
} else {
114+
// Unified profile share
115+
const { data, error } = await supabase
116+
.from("user_profiles")
117+
.select("persona_id, persona_name, persona_tagline, persona_confidence, total_repos, total_commits, axes_json, narrative_json")
118+
.eq("user_id", userId)
119+
.maybeSingle();
120+
121+
if (error) {
122+
console.error("Supabase error:", error);
123+
throw new Error(`Profile fetch failed: ${error.message}`);
124+
}
125+
profile = data;
73126
}
74-
127+
75128
if (!profile) {
76-
console.error(`Profile not found for userId: ${userId}`);
77-
return new Response("Profile not found - check if userId is correct or if RLS policies prevent access (Service Role Key required for private profiles)", { status: 404 });
129+
console.error(`Profile not found for userId: ${userId}${jobId ? `, jobId: ${jobId}` : ""}`);
130+
return new Response("Profile not found", { status: 404 });
78131
}
79132

80133
// Prepare Data

apps/web/src/components/share/ShareActions.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,13 @@ export function ShareActions({
4646
shareHeadline,
4747
shareTemplate,
4848
entityId,
49+
userId,
4950
disabled = false,
5051
shareJson,
5152
}: ShareActionsProps) {
53+
// When userId is provided, use it as the path param and append jobId query
54+
const shareApiId = userId ?? entityId;
55+
const shareApiQuery = userId ? `?jobId=${entityId}` : "";
5256
const [copied, setCopied] = useState(false);
5357
const [copiedLink, setCopiedLink] = useState(false);
5458
const [shareFormat, setShareFormat] = useState<ShareFormat>("og");
@@ -134,7 +138,7 @@ export function ShareActions({
134138

135139
try {
136140
// 2. Fetch the image blob
137-
const url = `/api/share/${shareFormat}/${entityId}`;
141+
const url = `/api/share/${shareFormat}/${shareApiId}${shareApiQuery}`;
138142
const res = await fetch(url);
139143
if (!res.ok) throw new Error("generation_failed");
140144
const blob = await res.blob();
@@ -182,7 +186,7 @@ export function ShareActions({
182186
setDownloading(true);
183187
try {
184188
// Use the new backend API for consistent image generation
185-
const url = `/api/share/${shareFormat}/${entityId}`;
189+
const url = `/api/share/${shareFormat}/${shareApiId}${shareApiQuery}`;
186190
const res = await fetch(url);
187191
if (!res.ok) throw new Error("generation_failed");
188192
const blob = await res.blob();
@@ -202,7 +206,7 @@ export function ShareActions({
202206
setStoryDownloading(true);
203207
try {
204208
// Use the new backend API for story generation
205-
const url = `/api/share/story/${entityId}`;
209+
const url = `/api/share/story/${shareApiId}${shareApiQuery}`;
206210
const res = await fetch(url);
207211
if (!res.ok) throw new Error("story_failed");
208212
const blob = await res.blob();

apps/web/src/components/share/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ export interface ShareActionsProps {
5757
shareTemplate: ShareImageTemplate | null;
5858
/** Job ID or profile ID for filename */
5959
entityId: string;
60+
/** User ID for share API routes (when entityId is a jobId) */
61+
userId?: string;
6062
/** Whether share is disabled */
6163
disabled?: boolean;
6264
/** Optional story download endpoint */

0 commit comments

Comments
 (0)