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
+50
-14
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"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) {
|
export function sanitizeSiteContent(siteContent) {
|
||||||
if (!siteContent || typeof siteContent !== 'object' || Array.isArray(siteContent)) return {}
|
if (!siteContent || typeof siteContent !== 'object' || Array.isArray(siteContent)) return {}
|
||||||
|
|
||||||
@@ -345,6 +371,7 @@ export function sanitizeSiteContent(siteContent) {
|
|||||||
colossiansStudySections: sanitizeColossiansStudySections(siteContent.colossiansStudySections),
|
colossiansStudySections: sanitizeColossiansStudySections(siteContent.colossiansStudySections),
|
||||||
redirects: sanitizeRedirectRules(siteContent.redirects),
|
redirects: sanitizeRedirectRules(siteContent.redirects),
|
||||||
podcastFeaturedLinks: sanitizeFeaturedLinks(siteContent.podcastFeaturedLinks ?? DEFAULT_PODCAST_FEATURED_LINKS),
|
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) : '',
|
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) : '',
|
welcomeEmailSpotifyBtnLabel: typeof siteContent.welcomeEmailSpotifyBtnLabel === 'string' ? siteContent.welcomeEmailSpotifyBtnLabel.trim().slice(0, 80) : '',
|
||||||
welcomeEmailAppleBtnLabel: typeof siteContent.welcomeEmailAppleBtnLabel === 'string' ? siteContent.welcomeEmailAppleBtnLabel.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
|
// 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
|
// Aggregate hits for the range
|
||||||
const rangeRealHits = filteredDayKeys.reduce((sum, day) => sum + (state.hitStats.byDayReal?.[day] ?? 0), 0)
|
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
|
// Filter per-path stats by range — approximate using recent visitor rows scoped to range
|
||||||
const rangeVisitorRows = state.visitorStats.recentVisits.filter(row => {
|
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
|
return row.at >= cutoffDate
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -14,15 +14,20 @@ export async function fetchAllEpisodes() {
|
|||||||
}
|
}
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const timeout = setTimeout(() => controller.abort(), 8000)
|
const timeout = setTimeout(() => controller.abort(), 8000)
|
||||||
const response = await fetch(RSS_FEED_URL, { signal: controller.signal })
|
try {
|
||||||
clearTimeout(timeout)
|
const response = await fetch(RSS_FEED_URL, { signal: controller.signal })
|
||||||
if (!response.ok) throw new Error(`RSS fetch failed: ${response.status}`)
|
clearTimeout(timeout)
|
||||||
const xml = await response.text()
|
if (!response.ok) throw new Error(`RSS fetch failed: ${response.status}`)
|
||||||
rawXmlCache = xml
|
const xml = await response.text()
|
||||||
const episodes = parseRssItems(xml)
|
rawXmlCache = xml
|
||||||
episodesCache = episodes
|
const episodes = parseRssItems(xml)
|
||||||
episodesCacheAt = now
|
episodesCache = episodes
|
||||||
return episodes
|
episodesCacheAt = now
|
||||||
|
return episodes
|
||||||
|
} catch (err) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchRawFeedXml() {
|
export async function fetchRawFeedXml() {
|
||||||
|
|||||||
+2
-2
@@ -1885,11 +1885,11 @@ function NuggetsPage({ content }: { content: SiteContent }) {
|
|||||||
|
|
||||||
const [episodes, setEpisodes] = useState<NuggetsEpisode[]>([])
|
const [episodes, setEpisodes] = useState<NuggetsEpisode[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [configured, setConfigured] = useState(true)
|
const [configured, setConfigured] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/nuggets/episodes/all')
|
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 }) => {
|
.then((d: { episodes?: NuggetsEpisode[]; configured?: boolean }) => {
|
||||||
setEpisodes(Array.isArray(d.episodes) ? d.episodes : [])
|
setEpisodes(Array.isArray(d.episodes) ? d.episodes : [])
|
||||||
setConfigured(d.configured !== false)
|
setConfigured(d.configured !== false)
|
||||||
|
|||||||
Reference in new issue
Block a user