Skip to content

Commit df29b7b

Browse files
Your Namecursoragent
andcommitted
Harden production resilience when Supabase is unreachable.
Degrade the public club board to an empty feed instead of 502, fix auth hook deps, and add a journals ingest stub so builds stay clean on Vercel. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b1db331 commit df29b7b

5 files changed

Lines changed: 39 additions & 14 deletions

File tree

app/api/club/_shared.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,27 @@
44
* setup notice instead of an error.
55
*/
66
export function clubTablesMissing(
7-
error: { code?: string; message?: string } | null | undefined,
7+
error: { code?: string; message?: string; details?: string } | null | undefined,
88
): boolean {
9-
const msg = error?.message || "";
9+
const msg = `${error?.message || ""} ${error?.details || ""}`;
1010
return (
1111
error?.code === "42P01" ||
1212
error?.code === "PGRST205" ||
1313
/club_(posts|joins).*(does not exist|schema cache)/i.test(msg) ||
1414
/relation .* does not exist/i.test(msg)
1515
);
1616
}
17+
18+
/**
19+
* True when Supabase is unreachable (DNS, network, timeout). Public board
20+
* listing must degrade to an empty feed — never 502 — so guests can still use
21+
* the rest of the app when the cloud project is down or misconfigured.
22+
*/
23+
export function clubUpstreamUnavailable(
24+
error: { code?: string; message?: string; details?: string } | null | undefined,
25+
): boolean {
26+
const msg = `${error?.message || ""} ${error?.details || ""}`;
27+
return /fetch failed|ENOTFOUND|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|network|TypeError/i.test(
28+
msg,
29+
);
30+
}

app/api/club/posts/route.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { bad, handleError, ok, safeJson, enforceRateLimit, BadRequestError } from "../../_utils";
2-
import { clubTablesMissing } from "../_shared";
2+
import { clubTablesMissing, clubUpstreamUnavailable } from "../_shared";
33
import { createClient, isSupabaseConfigured } from "@/lib/supabase/server";
44
import { getAppUser } from "@/lib/auth";
55

@@ -33,7 +33,10 @@ export async function GET(req: Request) {
3333
if (kind && KINDS.has(kind)) query = query.eq("kind", kind);
3434
const { data, error } = await query;
3535
if (error) {
36-
if (clubTablesMissing(error)) return ok({ configured: false, posts: [] });
36+
// Missing tables OR dead/unreachable Supabase project: empty public feed.
37+
if (clubTablesMissing(error) || clubUpstreamUnavailable(error)) {
38+
return ok({ configured: false, posts: [] });
39+
}
3740
throw error;
3841
}
3942

@@ -53,6 +56,10 @@ export async function GET(req: Request) {
5356
posts: (data || []).map((p) => ({ ...p, joined: joinedIds.includes(p.id as string) })),
5457
});
5558
} catch (e) {
59+
const msg = e instanceof Error ? e.message : String(e);
60+
if (clubUpstreamUnavailable({ message: msg })) {
61+
return ok({ configured: false, posts: [] });
62+
}
5663
return handleError(e);
5764
}
5865
}

app/auth/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export default function AuthPage() {
3131
supabase.auth.getSession().then(({ data }) => {
3232
if (data.session) window.location.href = nextPath;
3333
});
34-
}, [configured]);
34+
}, [configured, nextPath]);
3535

3636
async function trackAuth(eventType: "signup" | "login" | "auth_failed") {
3737
try {

lib/journals/generated.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Optional deploy-time journal dataset.
3+
* Populated by `scripts/ingest-journals.mjs` when that pipeline runs.
4+
* Curated journals in international.ts / saudi.ts always take precedence.
5+
*/
6+
import type { JournalRecord } from "./types";
7+
8+
export const generatedJournals: JournalRecord[] = [];

lib/journals/index.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,12 @@ import type {
1818
} from "./types";
1919
import { internationalJournals } from "./international";
2020
import { saudiJournals } from "./saudi";
21+
import { generatedJournals as generatedFromIngest } from "./generated";
2122

22-
// Optional generated dataset. The file may not exist until the ingest script
23-
// runs at deploy time, so we load it defensively without a hard import.
24-
let generatedJournals: JournalRecord[] = [];
25-
try {
26-
const mod = require("./generated");
27-
if (Array.isArray(mod.generatedJournals)) generatedJournals = mod.generatedJournals;
28-
} catch {
29-
/* no generated dataset yet — curated only */
30-
}
23+
// Deploy-time ingest may populate generated.ts; default stub is an empty array.
24+
const generatedJournals: JournalRecord[] = Array.isArray(generatedFromIngest)
25+
? generatedFromIngest
26+
: [];
3127

3228
const STOPWORDS = new Set([
3329
"the", "a", "an", "of", "and", "or", "in", "on", "for", "to", "with", "by",

0 commit comments

Comments
 (0)