Skip to content

Commit ff9978e

Browse files
authored
Merge pull request #30 from werther41/prod-hotfix-1
Prod hotfix 1
2 parents 83b6f3f + 64ea2e2 commit ff9978e

File tree

13 files changed

+415
-50
lines changed

13 files changed

+415
-50
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Werther
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

app/api/health/db/route.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { NextResponse } from "next/server"
2+
3+
import { getConnectionStatus } from "@/lib/db-utils"
4+
5+
export async function GET() {
6+
try {
7+
const status = await getConnectionStatus()
8+
9+
return NextResponse.json(
10+
{
11+
status: status.connected ? "healthy" : "unhealthy",
12+
database: status,
13+
timestamp: new Date().toISOString(),
14+
},
15+
{
16+
status: status.connected ? 200 : 503,
17+
headers: {
18+
"Cache-Control": "no-cache, no-store, must-revalidate",
19+
Pragma: "no-cache",
20+
Expires: "0",
21+
},
22+
}
23+
)
24+
} catch (error) {
25+
console.error("Health check failed:", error)
26+
27+
return NextResponse.json(
28+
{
29+
status: "unhealthy",
30+
error: error instanceof Error ? error.message : "Unknown error",
31+
timestamp: new Date().toISOString(),
32+
},
33+
{
34+
status: 503,
35+
headers: {
36+
"Cache-Control": "no-cache, no-store, must-revalidate",
37+
Pragma: "no-cache",
38+
Expires: "0",
39+
},
40+
}
41+
)
42+
}
43+
}

app/api/topics/route.ts

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { NextRequest, NextResponse } from "next/server"
22

3+
import { FALLBACK_TOPICS, shouldUseFallback } from "@/lib/fallback-data"
34
import { getTrendingTopics } from "@/lib/topic-extraction"
45
import { getDiverseTopics } from "@/lib/topic-search"
56

6-
// Cache for 15 minutes
7-
export const revalidate = 900
7+
// Cache for 5 minutes
8+
export const revalidate = 300
89

910
export async function GET(request: NextRequest) {
1011
try {
@@ -35,16 +36,41 @@ export async function GET(request: NextRequest) {
3536
)
3637
}
3738

38-
// Get topics (diverse or regular)
39-
const topics = diverse
40-
? await getDiverseTopics({
41-
timeWindow,
42-
limit,
43-
entityType,
44-
randomize,
45-
topicTypes,
46-
})
47-
: await getTrendingTopics({ timeWindow, limit, entityType, topicTypes })
39+
// Get topics (diverse or regular) with fallback
40+
let topics
41+
try {
42+
topics = diverse
43+
? await getDiverseTopics({
44+
timeWindow,
45+
limit,
46+
entityType,
47+
randomize,
48+
topicTypes,
49+
})
50+
: await getTrendingTopics({ timeWindow, limit, entityType, topicTypes })
51+
52+
// If we got no topics and database might be having issues, use fallback
53+
if (topics.length === 0) {
54+
console.log("No topics found, checking database connectivity...")
55+
const { getConnectionStatus } = await import("@/lib/db-utils")
56+
const dbStatus = await getConnectionStatus()
57+
58+
if (!dbStatus.connected) {
59+
console.log("Database is not connected, using fallback topics")
60+
topics = FALLBACK_TOPICS.slice(0, limit)
61+
}
62+
}
63+
} catch (error) {
64+
console.error("Error fetching topics from database:", error)
65+
66+
// Use fallback data if it's a network/database error
67+
if (shouldUseFallback(error)) {
68+
console.log("Using fallback topics due to database connectivity issues")
69+
topics = FALLBACK_TOPICS.slice(0, limit)
70+
} else {
71+
throw error
72+
}
73+
}
4874

4975
// Transform to API response format
5076
const response = {
@@ -81,9 +107,9 @@ export async function GET(request: NextRequest) {
81107

82108
return NextResponse.json(response, {
83109
headers: {
84-
"Cache-Control": "public, s-maxage=900, stale-while-revalidate=1800",
85-
"CDN-Cache-Control": "public, s-maxage=900",
86-
"Vercel-CDN-Cache-Control": "public, s-maxage=900",
110+
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
111+
"CDN-Cache-Control": "public, s-maxage=300",
112+
"Vercel-CDN-Cache-Control": "public, s-maxage=300",
87113
},
88114
})
89115
} catch (error) {

app/discover/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ export default function DiscoverPage() {
377377
)}
378378

379379
<Badge variant="outline">
380-
<Clock className="mr-1 size-3" />
380+
<Clock className="mr-1 w-3 h-3" />
381381
{
382382
TIME_FILTER_OPTIONS.find(
383383
(opt) => opt.value === searchState.timeFilter
@@ -386,7 +386,7 @@ export default function DiscoverPage() {
386386
</Badge>
387387

388388
<Badge variant="outline">
389-
<Filter className="mr-1 size-3" />
389+
<Filter className="mr-1 w-3 h-3" />
390390
{searchState.sortBy === "score"
391391
? "Most Relevant"
392392
: "Most Recent"}

components/article-list.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export function ArticleList({
190190
onClick={() => onSortChange("time")}
191191
className="flex-1 sm:flex-none"
192192
>
193-
<Calendar className="mr-1 size-3 w-4 sm:h-4" />
193+
<Calendar className="mr-1 h-4 w-4 sm:h-4" />
194194
<span className="xs:inline hidden">Most Recent</span>
195195
<span className="xs:hidden">Recent</span>
196196
</Button>
@@ -213,11 +213,14 @@ export function ArticleList({
213213
className="border-primary/20 hover:border-primary/40 transition-colors"
214214
>
215215
<CardHeader className="pb-3">
216-
<div className="flex items-start justify-between gap-4">
216+
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
217217
<CardTitle className="text-lg leading-tight">
218218
{article.title}
219219
</CardTitle>
220-
<Badge variant="secondary" className="shrink-0">
220+
<Badge
221+
variant="secondary"
222+
className="shrink-0 self-start sm:self-auto"
223+
>
221224
{article.source}
222225
</Badge>
223226
</div>
@@ -262,7 +265,7 @@ export function ArticleList({
262265
variant="outline"
263266
className="border-blue-200 bg-blue-100 text-xs text-blue-800 hover:bg-blue-200 dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-300 dark:hover:bg-blue-900/30"
264267
>
265-
<Hash className="mr-1 size-3" />
268+
<Hash className="mr-1 w-3 h-3" />
266269
{topic}
267270
</Badge>
268271
))}

components/fact-page-client.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export function FactPageClient({ fact }: FactPageClientProps) {
8888
rel="noopener noreferrer"
8989
className="hover:text-primary/80 inline-flex items-center gap-1 text-primary transition-colors"
9090
>
91-
<ExternalLink className="size-3" />
91+
<ExternalLink className="w-3 h-3" />
9292
Read original article
9393
</a>
9494
)}

components/real-time-fact-section.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ export function RealTimeFactSection({ className }: RealTimeFactProps) {
337337
rel="noopener noreferrer"
338338
className="hover:text-primary/80 inline-flex items-center gap-1 text-primary transition-colors"
339339
>
340-
<ExternalLink className="size-3" />
340+
<ExternalLink className="w-3 h-3" />
341341
Read original article
342342
</a>
343343
)}

components/topic-selector.tsx

Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,9 @@ export function TopicSelector({
142142
"LAW_OR_POLICY",
143143
]
144144

145-
// Fetch trending topics
145+
// Fetch trending topics with retry logic
146146
useEffect(() => {
147-
const fetchTopics = async () => {
147+
const fetchTopics = async (retryCount = 0) => {
148148
try {
149149
setIsLoading(true)
150150
setError("")
@@ -154,23 +154,44 @@ export function TopicSelector({
154154
? `&topicTypes=${selectedTopicTypes.join(",")}`
155155
: ""
156156

157+
// Add cache-busting parameter to prevent stale cache issues
158+
const timestamp = Date.now()
157159
const url = enableDiversity
158-
? `/api/topics?limit=20&timeWindow=48&diverse=true${topicTypesParam}`
159-
: `/api/topics?limit=20&timeWindow=48${topicTypesParam}`
160+
? `/api/topics?limit=20&timeWindow=48&diverse=true&cache_bust=${timestamp}${topicTypesParam}`
161+
: `/api/topics?limit=20&timeWindow=48&cache_bust=${timestamp}${topicTypesParam}`
160162

161-
const response = await fetch(url)
163+
const response = await fetch(url, {
164+
cache: "no-store",
165+
headers: {
166+
"Cache-Control": "no-cache",
167+
Pragma: "no-cache",
168+
},
169+
})
162170

163171
if (!response.ok) {
164172
throw new Error(`Failed to fetch topics: ${response.status}`)
165173
}
166174

167175
const data = await response.json()
168176
setTopics(data.topics || [])
177+
setError("") // Clear any previous errors
178+
setIsLoading(false) // Always clear loading on successful response
169179
} catch (err) {
170180
console.error("Error fetching topics:", err)
171-
setError(err instanceof Error ? err.message : "Failed to load topics")
172-
} finally {
173-
setIsLoading(false)
181+
182+
// Retry logic: retry up to 3 times with exponential backoff
183+
if (retryCount < 3) {
184+
const delay = Math.pow(2, retryCount) * 1000 // 1s, 2s, 4s
185+
console.log(
186+
`Retrying fetch topics in ${delay}ms (attempt ${retryCount + 1}/3)`
187+
)
188+
setTimeout(() => {
189+
fetchTopics(retryCount + 1)
190+
}, delay)
191+
} else {
192+
setError(err instanceof Error ? err.message : "Failed to load topics")
193+
setIsLoading(false) // Clear loading on final failure
194+
}
174195
}
175196
}
176197

@@ -334,7 +355,49 @@ export function TopicSelector({
334355
<Button
335356
variant="outline"
336357
size="sm"
337-
onClick={() => window.location.reload()}
358+
onClick={() => {
359+
setError("")
360+
setIsLoading(true)
361+
// Trigger a new fetch by updating a dependency
362+
const timestamp = Date.now()
363+
const topicTypesParam =
364+
selectedTopicTypes.length > 0
365+
? `&topicTypes=${selectedTopicTypes.join(",")}`
366+
: ""
367+
const url = enableDiversity
368+
? `/api/topics?limit=20&timeWindow=48&diverse=true&cache_bust=${timestamp}${topicTypesParam}`
369+
: `/api/topics?limit=20&timeWindow=48&cache_bust=${timestamp}${topicTypesParam}`
370+
371+
fetch(url, {
372+
cache: "no-store",
373+
headers: {
374+
"Cache-Control": "no-cache",
375+
Pragma: "no-cache",
376+
},
377+
})
378+
.then((response) => {
379+
if (!response.ok)
380+
throw new Error(
381+
`Failed to fetch topics: ${response.status}`
382+
)
383+
return response.json()
384+
})
385+
.then((data) => {
386+
setTopics(data.topics || [])
387+
setError("")
388+
})
389+
.catch((err) => {
390+
console.error("Error fetching topics:", err)
391+
setError(
392+
err instanceof Error
393+
? err.message
394+
: "Failed to load topics"
395+
)
396+
})
397+
.finally(() => {
398+
setIsLoading(false)
399+
})
400+
}}
338401
>
339402
Try Again
340403
</Button>
@@ -362,12 +425,12 @@ export function TopicSelector({
362425
<Card className={`border-primary/20 ${className}`}>
363426
<CardContent className="p-3">
364427
<div className="mb-3">
365-
<div className="mb-2">
366-
<h3 className="mb-2 flex items-center gap-2 text-base font-semibold sm:mb-0">
428+
<div className="mb-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
429+
<h3 className="flex items-center gap-2 text-base font-semibold">
367430
<TrendingUp className="h-4 w-4" />
368431
{showSearchResults ? "Search Results" : "Trending Topics"}
369432
</h3>
370-
<div className="flex flex-wrap items-center gap-2 sm:justify-end">
433+
<div className="flex flex-wrap items-center gap-2">
371434
{!showSearchResults && (
372435
<Button
373436
variant="ghost"
@@ -424,11 +487,6 @@ export function TopicSelector({
424487
{/* Topic Type Filter */}
425488
{enableTopicTypeFilter && (
426489
<div className="mb-3">
427-
<div className="mb-1">
428-
<span className="text-xs font-medium text-muted-foreground">
429-
Filter by type:
430-
</span>
431-
</div>
432490
<div className="flex flex-wrap gap-1">
433491
{availableTopicTypes.map((type) => {
434492
const isSelected = selectedTopicTypes.includes(type)

0 commit comments

Comments
 (0)