Fix 5 bugs: analytics range keys, stale visitor rows, NuggetsPage init state, episodes timer leak, unsanitized nuggets object; v2.0.1
Publish Container / docker (push) Successful in 14m54s
Publish Container / docker (push) Successful in 14m54s
- analytics: union byDayReal+byDayBot keys so bot-only days aren't dropped from range totals - analytics: exclude timestamp-less visitor rows from range filters (was unconditionally including them) - App.tsx: init `configured` to false so a network failure doesn't show a misleading "no episodes" state; add r.ok guard before r.json() - episodes.js: wrap fetch in try/catch so clearTimeout is called on network failure, matching the nuggets.js pattern - helpers.js: add sanitizeNuggets + wire it into sanitizeSiteContent so corrupted hosts/urls can't crash NuggetsPage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
31e79ad542
commit
4378fad66f
5 files changed
+41
-5
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "siteforge",
|
||||
"private": true,
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -330,6 +330,32 @@ function sanitizeArchivedSeries(value) {
|
||||
)
|
||||
}
|
||||
|
||||
function sanitizeNuggets(raw) {
|
||||
const n = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {}
|
||||
const hosts = Array.isArray(n.hosts)
|
||||
? n.hosts
|
||||
.filter(h => h && typeof h === 'object' && typeof h.id === 'string' && h.id.trim())
|
||||
.map(h => ({
|
||||
id: h.id.trim(),
|
||||
name: typeof h.name === 'string' ? h.name.trim().slice(0, 120) : '',
|
||||
role: typeof h.role === 'string' ? h.role.trim().slice(0, 120) : '',
|
||||
bio: typeof h.bio === 'string' ? h.bio.trim().slice(0, 600) : '',
|
||||
imageUrl: sanitizeUrl(h.imageUrl) || '',
|
||||
archived: Boolean(h.archived),
|
||||
}))
|
||||
: []
|
||||
return {
|
||||
title: typeof n.title === 'string' ? n.title.trim().slice(0, 120) : '',
|
||||
tagline: typeof n.tagline === 'string' ? n.tagline.trim().slice(0, 240) : '',
|
||||
description: typeof n.description === 'string' ? n.description.trim().slice(0, 600) : '',
|
||||
podcastArtUrl: sanitizeUrl(n.podcastArtUrl) || '',
|
||||
rssFeedUrl: sanitizeUrl(n.rssFeedUrl) || '',
|
||||
spotifyUrl: sanitizeUrl(n.spotifyUrl) || '',
|
||||
appleUrl: sanitizeUrl(n.appleUrl) || '',
|
||||
hosts,
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeSiteContent(siteContent) {
|
||||
if (!siteContent || typeof siteContent !== 'object' || Array.isArray(siteContent)) return {}
|
||||
|
||||
@@ -345,6 +371,7 @@ export function sanitizeSiteContent(siteContent) {
|
||||
colossiansStudySections: sanitizeColossiansStudySections(siteContent.colossiansStudySections),
|
||||
redirects: sanitizeRedirectRules(siteContent.redirects),
|
||||
podcastFeaturedLinks: sanitizeFeaturedLinks(siteContent.podcastFeaturedLinks ?? DEFAULT_PODCAST_FEATURED_LINKS),
|
||||
nuggets: sanitizeNuggets(siteContent.nuggets),
|
||||
episodesSeoIntro: typeof siteContent.episodesSeoIntro === 'string' && siteContent.episodesSeoIntro.trim() ? siteContent.episodesSeoIntro.trim().slice(0, 600) : '',
|
||||
welcomeEmailSpotifyBtnLabel: typeof siteContent.welcomeEmailSpotifyBtnLabel === 'string' ? siteContent.welcomeEmailSpotifyBtnLabel.trim().slice(0, 80) : '',
|
||||
welcomeEmailAppleBtnLabel: typeof siteContent.welcomeEmailAppleBtnLabel === 'string' ? siteContent.welcomeEmailAppleBtnLabel.trim().slice(0, 80) : '',
|
||||
|
||||
@@ -213,7 +213,11 @@ export function register(app) {
|
||||
})()
|
||||
|
||||
// Filter byDayReal/byDayBot keys to only those within the range
|
||||
const filteredDayKeys = Object.keys(state.hitStats.byDayReal ?? {}).filter(day => day >= cutoffDate)
|
||||
const allDayKeys = new Set([
|
||||
...Object.keys(state.hitStats.byDayReal ?? {}),
|
||||
...Object.keys(state.hitStats.byDayBot ?? {}),
|
||||
])
|
||||
const filteredDayKeys = [...allDayKeys].filter(day => day >= cutoffDate)
|
||||
|
||||
// Aggregate hits for the range
|
||||
const rangeRealHits = filteredDayKeys.reduce((sum, day) => sum + (state.hitStats.byDayReal?.[day] ?? 0), 0)
|
||||
@@ -222,7 +226,7 @@ export function register(app) {
|
||||
|
||||
// Filter per-path stats by range — approximate using recent visitor rows scoped to range
|
||||
const rangeVisitorRows = state.visitorStats.recentVisits.filter(row => {
|
||||
if (!row.at) return true // include if no timestamp
|
||||
if (!row.at) return false // exclude rows with no timestamp from range views
|
||||
return row.at >= cutoffDate
|
||||
})
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export async function fetchAllEpisodes() {
|
||||
}
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), 8000)
|
||||
try {
|
||||
const response = await fetch(RSS_FEED_URL, { signal: controller.signal })
|
||||
clearTimeout(timeout)
|
||||
if (!response.ok) throw new Error(`RSS fetch failed: ${response.status}`)
|
||||
@@ -23,6 +24,10 @@ export async function fetchAllEpisodes() {
|
||||
episodesCache = episodes
|
||||
episodesCacheAt = now
|
||||
return episodes
|
||||
} catch (err) {
|
||||
clearTimeout(timeout)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchRawFeedXml() {
|
||||
|
||||
+2
-2
@@ -1885,11 +1885,11 @@ function NuggetsPage({ content }: { content: SiteContent }) {
|
||||
|
||||
const [episodes, setEpisodes] = useState<NuggetsEpisode[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [configured, setConfigured] = useState(true)
|
||||
const [configured, setConfigured] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/nuggets/episodes/all')
|
||||
.then(r => r.json())
|
||||
.then(r => r.ok ? r.json() : Promise.reject(r.status))
|
||||
.then((d: { episodes?: NuggetsEpisode[]; configured?: boolean }) => {
|
||||
setEpisodes(Array.isArray(d.episodes) ? d.episodes : [])
|
||||
setConfigured(d.configured !== false)
|
||||
|
||||
Reference in new issue
Block a user