mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 05:42:22 +00:00
Hold Cockpit regional news to the last 24 hours, and tell its states apart
Regional News showed "REGIONAL NEWS UNAVAILABLE" above populated rows and a
static "RECENT" footer over months-old articles. The status banner kept
`display: grid` when hidden, every non-empty answer carried the unavailable
text, and nothing limited or sorted articles by age.
- One window (regionalNewsWindow.js, 24 h): the local proxy asks Google News
with `when:1d` and GDELT for 24 h, drops undated, older or future-dated
articles, sorts newest first, and reports newsWindowHours/newsFetchedAt.
- The Cockpit labels four states: fresh (banner hidden, "LAST 24 H"),
kept after a failed refresh ("CACHED · REFRESH FAILED"), no news in the
window, and unavailable. The footer lives in the news state, so it
survives page and tab changes, and the client drops out-of-window rows.
- `.cockpit-brief-status[hidden]` now hides the banner.
This commit is contained in:
1 parent
2470b39f88
commit
dd83812a21
24 files changed
+637
-85
No files matched your search
@@ -3,6 +3,7 @@ import { fetchRegionalPlace } from './place.js';
|
||||
import { fetchRegionalWeather } from './weather.js';
|
||||
import { fetchRegionalNews } from './news.js';
|
||||
import { validRegionalPoint } from './query.js';
|
||||
import { REGIONAL_NEWS_WINDOW_HOURS } from '../../../src/data/regionalModel.js';
|
||||
import { coalesceProxyRequest } from '../common/http.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -50,10 +51,11 @@ function regionalBriefProxy({ placeProvider = fetchRegionalPlace } = {}) {
|
||||
if (!regionalBriefHasAnySource({ place, weather, news })) {
|
||||
throw new Error('All regional briefing sources unavailable');
|
||||
}
|
||||
const retrievedAt = new Date().toISOString();
|
||||
const payload = {
|
||||
status:
|
||||
place && weather && news.status !== 'unavailable' ? 'ready' : 'partial',
|
||||
retrievedAt: new Date().toISOString(),
|
||||
retrievedAt,
|
||||
coordinates: point,
|
||||
place,
|
||||
placeStatus: place ? 'ready' : 'unavailable',
|
||||
@@ -62,6 +64,9 @@ function regionalBriefProxy({ placeProvider = fetchRegionalPlace } = {}) {
|
||||
newsStatus: news.status,
|
||||
newsQuery: news.query,
|
||||
newsSource: news.source,
|
||||
newsWindowHours: REGIONAL_NEWS_WINDOW_HOURS,
|
||||
// A cached or stale answer keeps this, so the client can show its age.
|
||||
newsFetchedAt: retrievedAt,
|
||||
articles: news.articles,
|
||||
};
|
||||
_regionalBriefCache.set(key, { payload, cachedAt: Date.now() });
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { fetchRegionalText, fetchRegionalJson } from './http.js';
|
||||
import { normalizeRegionalArticles } from '../../../src/data/regionalModel.js';
|
||||
import {
|
||||
REGIONAL_NEWS_WINDOW_HOURS,
|
||||
googleNewsWindowOperator,
|
||||
normalizeRegionalArticles,
|
||||
recentRegionalArticles,
|
||||
} from '../../../src/data/regionalModel.js';
|
||||
|
||||
function decodeRssText(value) {
|
||||
return String(value || '')
|
||||
@@ -22,7 +27,10 @@ function rssTag(block, tag) {
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeRssArticles(xml, limit = 5) {
|
||||
// Feed items read before the window filter; Google returns about 100.
|
||||
const RSS_ITEM_CAP = 100;
|
||||
|
||||
function normalizeRssArticles(xml, limit = RSS_ITEM_CAP) {
|
||||
const seen = new Set();
|
||||
const articles = [];
|
||||
for (const match of String(xml || '').matchAll(
|
||||
@@ -57,12 +65,27 @@ function normalizeRssArticles(xml, limit = 5) {
|
||||
return articles;
|
||||
}
|
||||
|
||||
async function fetchRegionalNews(place) {
|
||||
/**
|
||||
* Headlines for a place from the last REGIONAL_NEWS_WINDOW_HOURS, newest
|
||||
* first: Google News RSS, then GDELT when RSS fails or has nothing recent.
|
||||
* `empty` means a source answered with nothing in the window; `unavailable`
|
||||
* means there was no place or no source answered.
|
||||
* @param {{locality?: string, region?: string, country?: string}|null} place
|
||||
* @param {{now?: () => number}} [options]
|
||||
*/
|
||||
async function fetchRegionalNews(place, { now = Date.now } = {}) {
|
||||
const query = place?.locality || place?.region || place?.country;
|
||||
if (!query)
|
||||
return { status: 'unavailable', query: null, articles: [], source: null };
|
||||
const recent = (articles) =>
|
||||
recentRegionalArticles(articles, {
|
||||
now: now(),
|
||||
windowHours: REGIONAL_NEWS_WINDOW_HOURS,
|
||||
limit: 5,
|
||||
});
|
||||
let answered = false;
|
||||
const rssParams = new URLSearchParams({
|
||||
q: String(query).replace(/["\\]/g, ' ').trim(),
|
||||
q: `${String(query).replace(/["\\]/g, ' ').trim()} ${googleNewsWindowOperator()}`,
|
||||
hl: 'en-US',
|
||||
gl: 'US',
|
||||
ceid: 'US:en',
|
||||
@@ -75,7 +98,8 @@ async function fetchRegionalNews(place) {
|
||||
timeoutMs: 12_000,
|
||||
},
|
||||
);
|
||||
const articles = normalizeRssArticles(xml, 5);
|
||||
answered = true;
|
||||
const articles = recent(normalizeRssArticles(xml));
|
||||
if (articles.length)
|
||||
return { status: 'ready', query, articles, source: 'Google News RSS' };
|
||||
} catch {
|
||||
@@ -87,7 +111,7 @@ async function fetchRegionalNews(place) {
|
||||
format: 'json',
|
||||
maxrecords: '5',
|
||||
sort: 'datedesc',
|
||||
timespan: '48h',
|
||||
timespan: `${REGIONAL_NEWS_WINDOW_HOURS}h`,
|
||||
});
|
||||
try {
|
||||
const payload = await fetchRegionalJson(
|
||||
@@ -97,7 +121,7 @@ async function fetchRegionalNews(place) {
|
||||
timeoutMs: 12_000,
|
||||
},
|
||||
);
|
||||
const articles = normalizeRegionalArticles(payload, 5);
|
||||
const articles = recent(normalizeRegionalArticles(payload, 5));
|
||||
return {
|
||||
status: articles.length ? 'ready' : 'empty',
|
||||
query,
|
||||
@@ -105,7 +129,10 @@ async function fetchRegionalNews(place) {
|
||||
source: 'GDELT fallback',
|
||||
};
|
||||
} catch {
|
||||
return { status: 'unavailable', query, articles: [], source: null };
|
||||
// An RSS answer with nothing recent is still a true empty result.
|
||||
return answered
|
||||
? { status: 'empty', query, articles: [], source: 'Google News RSS' }
|
||||
: { status: 'unavailable', query, articles: [], source: null };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user