Show news from the window the server serves

A host may serve fewer hours than the day. The hotspots answer may name its window
(`windowHours`, a whole number from 1 to 24); the layer keeps reports, the
feed, In view and the chosen event inside it, and the panel says
"Last 12 h" or "Last 24 h" accordingly. Without a window named, or with one
out of range, the layer keeps the full day as before.
This commit is contained in:
Denis Groshev committed 2026-11-06 00:45:27 +01:00
1 parent f3e206bf37
commit 60fad38c3f
18 files changed
+178 -73

No files matched your search

+2 -1
View File
@@ -1,5 +1,6 @@
- Add the News layer (Events ▸ News) and its right-rail NEWS panel: the last
24 hours from curated hotspots, coloured by severity only, with hotspot
24 hours from curated hotspots (or the fewer hours a server names, shown in
the panel), coloured by severity only, with hotspot
beacons, outlines with disputed areas, picture cards for major events, an
"In view" list below 3,000 km and a continuous feed whose cards fly the globe
to the event. Core serves no news itself: set `GEV_NEWS_ORIGIN` to a news
+4 -4
View File
@@ -82,17 +82,17 @@
"{severity} only": "{severity} فقط",
"All categories": "كل الفئات",
"Reading the news…": "جارٍ قراءة الأخبار…",
"News, last 24 hours": "الأخبار، آخر 24 ساعة",
"Last 24 h": "آخر 24 ساعة",
"News, last {hours} hours": "الأخبار، آخر {hours} ساعة",
"Last {hours} h": "آخر {hours} ساعة",
"Hotspots": "بؤر ساخنة",
"Severity": "الخطورة",
"Category": "الفئة",
"Include minor news": "تضمين الأخبار الثانوية",
"Show event cards": "إظهار بطاقات الأحداث",
"In view": "في مجال الرؤية",
"Nothing reported near here in the last 24 h": "لم يُبلَّغ عن شيء قريب من هنا في آخر 24 ساعة",
"Nothing reported near here in the last {hours} h": "لم يُبلَّغ عن شيء قريب من هنا في آخر {hours} ساعة",
"Latest news": "آخر الأخبار",
"No news matches these filters in the last 24 h": "لا أخبار تطابق هذه المرشحات في آخر 24 ساعة",
"No news matches these filters in the last {hours} h": "لا أخبار تطابق هذه المرشحات في آخر {hours} ساعة",
"Load older news": "تحميل أخبار أقدم",
"How to read the map": "كيف تقرأ الخريطة",
"Colour: severity · icon: type of event": "اللون: الخطورة · الرمز: نوع الحدث",
+4 -4
View File
@@ -70,17 +70,17 @@
"{severity} only": "Nur {severity}",
"All categories": "Alle Kategorien",
"Reading the news…": "Nachrichten werden gelesen…",
"News, last 24 hours": "Nachrichten der letzten 24 Stunden",
"Last 24 h": "Letzte 24 Std.",
"News, last {hours} hours": "Nachrichten der letzten {hours} Stunden",
"Last {hours} h": "Letzte {hours} Std.",
"Hotspots": "Brennpunkte",
"Severity": "Schweregrad",
"Category": "Kategorie",
"Include minor news": "Nebensächliche Meldungen einbeziehen",
"Show event cards": "Ereigniskarten zeigen",
"In view": "Im Blick",
"Nothing reported near here in the last 24 h": "In den letzten 24 h nichts in der Nähe gemeldet",
"Nothing reported near here in the last {hours} h": "In den letzten {hours} h nichts in der Nähe gemeldet",
"Latest news": "Neueste Nachrichten",
"No news matches these filters in the last 24 h": "Keine Nachrichten der letzten 24 h passen zu diesen Filtern",
"No news matches these filters in the last {hours} h": "Keine Nachrichten der letzten {hours} h passen zu diesen Filtern",
"Load older news": "Ältere Nachrichten laden",
"How to read the map": "So liest man die Karte",
"Colour: severity · icon: type of event": "Farbe: Schweregrad · Symbol: Art des Ereignisses",
+4 -4
View File
@@ -73,17 +73,17 @@
"{severity} only": "Solo {severity}",
"All categories": "Todas las categorías",
"Reading the news…": "Leyendo las noticias…",
"News, last 24 hours": "Noticias de las últimas 24 horas",
"Last 24 h": "Últimas 24 h",
"News, last {hours} hours": "Noticias de las últimas {hours} horas",
"Last {hours} h": "Últimas {hours} h",
"Hotspots": "Focos",
"Severity": "Gravedad",
"Category": "Categoría",
"Include minor news": "Incluir noticias menores",
"Show event cards": "Mostrar tarjetas de sucesos",
"In view": "A la vista",
"Nothing reported near here in the last 24 h": "Nada informado cerca de aquí en las últimas 24 h",
"Nothing reported near here in the last {hours} h": "Nada informado cerca de aquí en las últimas {hours} h",
"Latest news": "Últimas noticias",
"No news matches these filters in the last 24 h": "Ninguna noticia de las últimas 24 h coincide con estos filtros",
"No news matches these filters in the last {hours} h": "Ninguna noticia de las últimas {hours} h coincide con estos filtros",
"Load older news": "Cargar noticias anteriores",
"How to read the map": "Cómo leer el mapa",
"Colour: severity · icon: type of event": "Color: gravedad · icono: tipo de suceso",
+4 -4
View File
@@ -73,17 +73,17 @@
"{severity} only": "{severity} uniquement",
"All categories": "Toutes les catégories",
"Reading the news…": "Lecture des actualités…",
"News, last 24 hours": "Actualités des dernières 24 heures",
"Last 24 h": "Dernières 24 h",
"News, last {hours} hours": "Actualités des dernières {hours} heures",
"Last {hours} h": "Dernières {hours} h",
"Hotspots": "Foyers",
"Severity": "Gravité",
"Category": "Catégorie",
"Include minor news": "Inclure les actualités secondaires",
"Show event cards": "Afficher les cartes d’événement",
"In view": "Dans la vue",
"Nothing reported near here in the last 24 h": "Rien de signalé près d’ici ces dernières 24 h",
"Nothing reported near here in the last {hours} h": "Rien de signalé près d’ici ces dernières {hours} h",
"Latest news": "Dernières actualités",
"No news matches these filters in the last 24 h": "Aucune actualité des dernières 24 h ne correspond à ces filtres",
"No news matches these filters in the last {hours} h": "Aucune actualité des dernières {hours} h ne correspond à ces filtres",
"Load older news": "Charger des actualités plus anciennes",
"How to read the map": "Comment lire la carte",
"Colour: severity · icon: type of event": "Couleur : gravité · icône : type d’événement",
+4 -4
View File
@@ -73,17 +73,17 @@
"{severity} only": "Solo {severity}",
"All categories": "Tutte le categorie",
"Reading the news…": "Lettura delle notizie…",
"News, last 24 hours": "Notizie delle ultime 24 ore",
"Last 24 h": "Ultime 24 h",
"News, last {hours} hours": "Notizie delle ultime {hours} ore",
"Last {hours} h": "Ultime {hours} h",
"Hotspots": "Focolai",
"Severity": "Gravità",
"Category": "Categoria",
"Include minor news": "Includi notizie minori",
"Show event cards": "Mostra le schede degli eventi",
"In view": "In vista",
"Nothing reported near here in the last 24 h": "Nulla segnalato qui vicino nelle ultime 24 h",
"Nothing reported near here in the last {hours} h": "Nulla segnalato qui vicino nelle ultime {hours} h",
"Latest news": "Ultime notizie",
"No news matches these filters in the last 24 h": "Nessuna notizia delle ultime 24 h corrisponde a questi filtri",
"No news matches these filters in the last {hours} h": "Nessuna notizia delle ultime {hours} h corrisponde a questi filtri",
"Load older news": "Carica notizie precedenti",
"How to read the map": "Come leggere la mappa",
"Colour: severity · icon: type of event": "Colore: gravità · icona: tipo di evento",
+4 -4
View File
@@ -67,17 +67,17 @@
"{severity} only": "{severity}のみ",
"All categories": "すべてのカテゴリ",
"Reading the news…": "ニュースを読み込み中…",
"News, last 24 hours": "直近24時間のニュース",
"Last 24 h": "直近24時間",
"News, last {hours} hours": "直近{hours}時間のニュース",
"Last {hours} h": "直近{hours}時間",
"Hotspots": "ホットスポット",
"Severity": "深刻度",
"Category": "カテゴリ",
"Include minor news": "軽微なニュースも含める",
"Show event cards": "イベントカードを表示",
"In view": "表示範囲内",
"Nothing reported near here in the last 24 h": "直近24時間、この付近の報道はありません",
"Nothing reported near here in the last {hours} h": "直近{hours}時間、この付近の報道はありません",
"Latest news": "最新ニュース",
"No news matches these filters in the last 24 h": "直近24時間で条件に合うニュースはありません",
"No news matches these filters in the last {hours} h": "直近{hours}時間で条件に合うニュースはありません",
"Load older news": "以前のニュースを読み込む",
"How to read the map": "地図の見方",
"Colour: severity · icon: type of event": "色: 深刻度 · アイコン: 出来事の種類",
+4 -4
View File
@@ -73,17 +73,17 @@
"{severity} only": "Apenas {severity}",
"All categories": "Todas as categorias",
"Reading the news…": "Lendo as notícias…",
"News, last 24 hours": "Notícias das últimas 24 horas",
"Last 24 h": "Últimas 24 h",
"News, last {hours} hours": "Notícias das últimas {hours} horas",
"Last {hours} h": "Últimas {hours} h",
"Hotspots": "Focos",
"Severity": "Gravidade",
"Category": "Categoria",
"Include minor news": "Incluir notícias menores",
"Show event cards": "Mostrar cartões de eventos",
"In view": "Na vista",
"Nothing reported near here in the last 24 h": "Nada noticiado aqui perto nas últimas 24 h",
"Nothing reported near here in the last {hours} h": "Nada noticiado aqui perto nas últimas {hours} h",
"Latest news": "Últimas notícias",
"No news matches these filters in the last 24 h": "Nenhuma notícia das últimas 24 h corresponde a estes filtros",
"No news matches these filters in the last {hours} h": "Nenhuma notícia das últimas {hours} h corresponde a estes filtros",
"Load older news": "Carregar notícias anteriores",
"How to read the map": "Como ler o mapa",
"Colour: severity · icon: type of event": "Cor: gravidade · ícone: tipo de evento",
+4 -4
View File
@@ -76,17 +76,17 @@
"{severity} only": "Только {severity}",
"All categories": "Все категории",
"Reading the news…": "Читаем новости…",
"News, last 24 hours": "Новости за последние 24 часа",
"Last 24 h": "За 24 ч",
"News, last {hours} hours": "Новости за последние {hours} ч",
"Last {hours} h": "За {hours} ч",
"Hotspots": "Горячие точки",
"Severity": "Тяжесть",
"Category": "Категория",
"Include minor news": "Показывать второстепенные новости",
"Show event cards": "Показывать карточки событий",
"In view": "В поле зрения",
"Nothing reported near here in the last 24 h": "За последние 24 ч поблизости ничего не сообщалось",
"Nothing reported near here in the last {hours} h": "За последние {hours} ч поблизости ничего не сообщалось",
"Latest news": "Последние новости",
"No news matches these filters in the last 24 h": "За последние 24 ч нет новостей по этим фильтрам",
"No news matches these filters in the last {hours} h": "За последние {hours} ч нет новостей по этим фильтрам",
"Load older news": "Загрузить более ранние новости",
"How to read the map": "Как читать карту",
"Colour: severity · icon: type of event": "Цвет: тяжесть · значок: тип события",
+4 -4
View File
@@ -76,17 +76,17 @@
"{severity} only": "Лише {severity}",
"All categories": "Усі категорії",
"Reading the news…": "Читаємо новини…",
"News, last 24 hours": "Новини за останні 24 години",
"Last 24 h": "За 24 год",
"News, last {hours} hours": "Новини за останні {hours} год",
"Last {hours} h": "За {hours} год",
"Hotspots": "Гарячі точки",
"Severity": "Тяжкість",
"Category": "Категорія",
"Include minor news": "Показувати другорядні новини",
"Show event cards": "Показувати картки подій",
"In view": "У полі зору",
"Nothing reported near here in the last 24 h": "За останні 24 год поблизу нічого не повідомлялося",
"Nothing reported near here in the last {hours} h": "За останні {hours} год поблизу нічого не повідомлялося",
"Latest news": "Останні новини",
"No news matches these filters in the last 24 h": "За останні 24 год немає новин за цими фільтрами",
"No news matches these filters in the last {hours} h": "За останні {hours} год немає новин за цими фільтрами",
"Load older news": "Завантажити давніші новини",
"How to read the map": "Як читати мапу",
"Colour: severity · icon: type of event": "Колір: тяжкість · значок: тип події",
+4 -4
View File
@@ -67,17 +67,17 @@
"{severity} only": "仅{severity}",
"All categories": "所有类别",
"Reading the news…": "正在读取新闻…",
"News, last 24 hours": "最近 24 小时的新闻",
"Last 24 h": "最近 24 小时",
"News, last {hours} hours": "最近 {hours} 小时的新闻",
"Last {hours} h": "最近 {hours} 小时",
"Hotspots": "热点",
"Severity": "严重程度",
"Category": "类别",
"Include minor news": "包括次要新闻",
"Show event cards": "显示事件卡片",
"In view": "视野内",
"Nothing reported near here in the last 24 h": "最近 24 小时附近无报道",
"Nothing reported near here in the last {hours} h": "最近 {hours} 小时附近无报道",
"Latest news": "最新新闻",
"No news matches these filters in the last 24 h": "最近 24 小时没有符合这些筛选条件的新闻",
"No news matches these filters in the last {hours} h": "最近 {hours} 小时没有符合这些筛选条件的新闻",
"Load older news": "加载更早的新闻",
"How to read the map": "如何解读地图",
"Colour: severity · icon: type of event": "颜色:严重程度 · 图标:事件类型",
+4 -4
View File
@@ -67,17 +67,17 @@
"{severity} only": "僅{severity}",
"All categories": "所有類別",
"Reading the news…": "正在讀取新聞…",
"News, last 24 hours": "最近 24 小時的新聞",
"Last 24 h": "最近 24 小時",
"News, last {hours} hours": "最近 {hours} 小時的新聞",
"Last {hours} h": "最近 {hours} 小時",
"Hotspots": "熱點",
"Severity": "嚴重程度",
"Category": "類別",
"Include minor news": "包括次要新聞",
"Show event cards": "顯示事件卡片",
"In view": "視野內",
"Nothing reported near here in the last 24 h": "最近 24 小時附近無報導",
"Nothing reported near here in the last {hours} h": "最近 {hours} 小時附近無報導",
"Latest news": "最新新聞",
"No news matches these filters in the last 24 h": "最近 24 小時沒有符合這些篩選條件的新聞",
"No news matches these filters in the last {hours} h": "最近 {hours} 小時沒有符合這些篩選條件的新聞",
"Load older news": "載入更早的新聞",
"How to read the map": "如何解讀地圖",
"Colour: severity · icon: type of event": "顏色:嚴重程度 · 圖示:事件類型",
+18 -7
View File
@@ -29,6 +29,7 @@ import {
NEWS_RELEVANT,
NEWS_SETTLE_MS,
NEWS_SEVERITY,
NEWS_WINDOW_MS,
ageWords,
attestation,
beaconPixelSize,
@@ -44,6 +45,7 @@ import {
markerStyle,
mediaLabels,
mergeFeedPage,
newsWindowMs,
normalizeFilters,
passesFilters,
reportMarkerPosition,
@@ -139,6 +141,9 @@ export function createNewsLayer({
lastRunAt = null,
autoPages = 0;
let hotspots = [];
// The window the server serves (newsWindowMs): a host may serve
// fewer hours than the day.
let windowMs = NEWS_WINDOW_MS;
let mapReports = [];
let feed = emptyFeed();
let inView = emptyInView();
@@ -159,7 +164,7 @@ export function createNewsLayer({
inView.minor?.find((r) => r.id === id) ||
null;
const selected = () => (selectedId ? findReport(selectedId) : null);
const onMap = (report) => visibleOnMap(report, filters, now());
const onMap = (report) => visibleOnMap(report, filters, now(), windowMs);
function markerCartesian(report, pin = 0) {
const { lat, lon } = reportMarkerPosition(report, pin);
@@ -806,7 +811,7 @@ export function createNewsLayer({
}
const shownCount = () =>
feed.items.filter((r) => passesFilters(r, filters, now())).length;
feed.items.filter((r) => passesFilters(r, filters, now(), windowMs)).length;
async function loadFeed(mode) {
if (!enabled || unavailable) return;
@@ -828,7 +833,7 @@ export function createNewsLayer({
if (controller.signal.aborted || feedRequest !== controller || !enabled)
return;
feed = {
...mergeFeedPage(feed, page, mode, now()),
...mergeFeedPage(feed, page, mode, now(), undefined, windowMs),
loading: false,
error: null,
};
@@ -915,7 +920,7 @@ export function createNewsLayer({
if (controller.signal.aborted || inViewRequest !== controller) return;
inView = {
...inView,
minor: page.reports.filter((r) => withinNewsWindow(r, now())),
minor: page.reports.filter((r) => withinNewsWindow(r, now(), windowMs)),
loading: false,
error: null,
};
@@ -1015,7 +1020,7 @@ export function createNewsLayer({
color: level.color,
})),
items: feed.items
.filter((r) => passesFilters(r, filters, at))
.filter((r) => passesFilters(r, filters, at, windowMs))
.map((r) => feedCardView(r, at)),
inView: {
active: inView.active,
@@ -1026,10 +1031,12 @@ export function createNewsLayer({
inView.centre,
filters,
at,
undefined,
windowMs,
).map((r) => feedCardView(r, at)),
},
selected:
report && withinNewsWindow(report, at)
report && withinNewsWindow(report, at, windowMs)
? feedCardView(report, at)
: null,
hasMore: Boolean(feed.nextBefore),
@@ -1037,6 +1044,7 @@ export function createNewsLayer({
// Read failures are English data; the panel shows them translated.
error: error || feed.error ? t(error || feed.error) : null,
lastRunAt,
windowHours: windowMs / 3_600_000,
};
}
@@ -1149,7 +1157,10 @@ export function createNewsLayer({
hotspots = index.hotspots;
loadOutlines();
lastRunAt = index.lastRunAt;
mapReports = todays.reports.filter((r) => withinNewsWindow(r, now()));
windowMs = newsWindowMs(index.windowHours);
mapReports = todays.reports.filter((r) =>
withinNewsWindow(r, now(), windowMs),
);
filters = normalizeFilters({}, hotspots, filters);
error = null;
lastUpdate = now();
+42 -15
View File
@@ -11,6 +11,16 @@ export const NEWS_FEED_PAGE = 60;
export const NEWS_MAP_LIMIT = 500;
/** News is real time only: nothing older than a day is shown anywhere. */
export const NEWS_WINDOW_MS = 24 * 3_600_000;
/**
* The window a server serves, in ms: its `windowHours` (a whole number of
* hours up to 24; a host may serve fewer), else the full day.
*/
export function newsWindowMs(hours) {
return Number.isInteger(hours) && hours >= 1 && hours <= 24
? hours * 3_600_000
: NEWS_WINDOW_MS;
}
// A publish time this far ahead of the clock is skew, not a future story
// (as data/regionalNewsWindow.js).
const NEWS_CLOCK_SKEW_MS = 15 * 60_000;
@@ -210,21 +220,26 @@ const lastHeard = (report) =>
Date.parse(report?.event?.lastAt || report?.publishedAt || '');
/**
* Whether a time is inside the last 24 hours (the only news shown); one more
* than 15 minutes ahead of the clock is not.
* Whether a time is inside the window (the last 24 hours unless the server
* serves fewer; the only news shown); one more than 15 minutes ahead of the
* clock is not.
*/
function inWindow(value, now) {
function inWindow(value, now, windowMs = NEWS_WINDOW_MS) {
const at = Date.parse(value || '');
return (
Number.isFinite(at) &&
now - at <= NEWS_WINDOW_MS &&
now - at <= windowMs &&
at - now <= NEWS_CLOCK_SKEW_MS
);
}
/** Whether a report is from the last 24 hours (the only news shown). */
export function withinNewsWindow(report, now = Date.now()) {
return inWindow(report?.publishedAt, now);
/** Whether a report is inside the window (the only news shown). */
export function withinNewsWindow(
report,
now = Date.now(),
windowMs = NEWS_WINDOW_MS,
) {
return inWindow(report?.publishedAt, now, windowMs);
}
/** The event's one-report-per-side list, inside the last 24 hours only. */
@@ -548,7 +563,7 @@ export function reportMarkerPosition(report, pin = 0) {
/**
* Merge a page into the feed, newest first, one row per report id, keeping
* only the last 24 hours and at most `max` rows (a long session does not grow
* only the window and at most `max` rows (a long session does not grow
* the list). `mode` 'head' refreshes the top and keeps the older cursor;
* 'append' adds an older page and takes its cursor; 'replace' starts over.
*/
@@ -558,6 +573,7 @@ export function mergeFeedPage(
mode,
now = Date.now(),
max = NEWS_FEED_MAX,
windowMs = NEWS_WINDOW_MS,
) {
const byId = new Map();
const items =
@@ -567,7 +583,7 @@ export function mergeFeedPage(
? [...feed.items, ...page.reports]
: [...page.reports, ...feed.items];
for (const item of items)
if (!byId.has(item.id) && withinNewsWindow(item, now))
if (!byId.has(item.id) && withinNewsWindow(item, now, windowMs))
byId.set(item.id, item);
return {
items: [...byId.values()]
@@ -604,24 +620,34 @@ export function normalizeFilters(next = {}, hotspots = [], previous = {}) {
}
/** Whether a report passes the filters the server does not apply. */
export function passesFilters(report, filters, now = Date.now()) {
export function passesFilters(
report,
filters,
now = Date.now(),
windowMs = NEWS_WINDOW_MS,
) {
return (
withinNewsWindow(report, now) &&
withinNewsWindow(report, now, windowMs) &&
reportSeverity(report) >= filters.minSeverity
);
}
/** Whether a report belongs on the map under the current filters. */
export function visibleOnMap(report, filters, now = Date.now()) {
export function visibleOnMap(
report,
filters,
now = Date.now(),
windowMs = NEWS_WINDOW_MS,
) {
return (
(filters.hotspot === 'all' ||
(report.hotspotIds || [report.hotspotId]).includes(filters.hotspot)) &&
(filters.category === 'all' || report.category === filters.category) &&
passesFilters(report, filters, now)
passesFilters(report, filters, now, windowMs)
);
}
/** API query for the feed under the current filters: always the last 24 h. */
/** API query for the feed under the current filters: the window only. */
export function feedQueryFor(filters, extra = {}) {
return {
events: true,
@@ -660,12 +686,13 @@ export function reportsInView(
filters,
now = Date.now(),
limit = NEWS_IN_VIEW_LIMIT,
windowMs = NEWS_WINDOW_MS,
) {
if (!centre) return [];
return reports
.filter(
(r) =>
passesFilters(r, filters, now) &&
passesFilters(r, filters, now, windowMs) &&
(filters.category === 'all' || r.category === filters.category) &&
r.pins?.some(
(pin) => distanceKm(pin, centre) <= NEWS_IN_VIEW_RADIUS_KM,
+38
View File
@@ -20,6 +20,7 @@ import {
isPressing,
markerStyle,
mergeFeedPage,
newsWindowMs,
normalizeFilters,
passesFilters,
reportMarkerPosition,
@@ -562,3 +563,40 @@ test('in view: half-degree cells and reports within 500 km of the centre', () =>
assert.deepEqual(reportsInView([near], null, filters, NOW), []);
assert.deepEqual(reportsInView([], centre, filters, NOW), []);
});
test('a server may serve fewer hours than the day, never more', () => {
assert.equal(newsWindowMs(12), 12 * 3_600_000);
assert.equal(newsWindowMs(24), 24 * 3_600_000);
for (const bad of [0, 25, 12.5, '12', null, undefined])
assert.equal(newsWindowMs(bad), 24 * 3_600_000);
const page = {
reports: [
report('fresh', '2026-10-28T02:00:00Z'),
report('old', '2026-10-27T20:00:00Z'),
],
nextBefore: null,
};
const empty = { items: [], nextBefore: null };
const ids = (windowMs) =>
mergeFeedPage(empty, page, 'replace', NOW, undefined, windowMs).items.map(
(r) => r.id,
);
assert.deepEqual(ids(newsWindowMs(12)), ['fresh']);
assert.deepEqual(ids(newsWindowMs(24)), ['fresh', 'old']);
});
test('the hotspots answer carries the window it was counted over', () => {
const base = {
hotspots: [],
ingest: { lastRunAt: '2026-10-28T09:00:00Z' },
};
assert.equal(
normalizeHotspotsPayload({ ...base, windowHours: 12 }).windowHours,
12,
);
assert.equal(normalizeHotspotsPayload(base).windowHours, null);
assert.equal(
normalizeHotspotsPayload({ ...base, windowHours: 48 }).windowHours,
null,
);
});
+8 -1
View File
@@ -96,7 +96,8 @@ function normalizeDeveloping(items) {
}
/**
* `/api/news/hotspots` → `{hotspots, categories, lastRunAt}`, or null when the
* `/api/news/hotspots` → `{hotspots, categories, lastRunAt, windowHours}`
* (`windowHours` null when the server names none), or null when the
* payload has no hotspot list.
*/
export function normalizeHotspotsPayload(payload) {
@@ -135,6 +136,12 @@ export function normalizeHotspotsPayload(payload) {
)
: [],
lastRunAt: iso(payload.ingest?.lastRunAt),
windowHours:
Number.isInteger(payload.windowHours) &&
payload.windowHours >= 1 &&
payload.windowHours <= 24
? payload.windowHours
: null,
};
}
+16 -5
View File
@@ -7,7 +7,8 @@ import UiSelect from '../kit/UiSelect.js';
import RailChips from '../rail/RailChips.vue';
import NewsCard from './NewsCard.vue';
// The News panel: the last 24 hours only. Hotspot chips, filters, the chosen
// The News panel: the window the server serves only (the last 24 hours, or
// fewer where the host serves less). Hotspot chips, filters, the chosen
// event, "In view" (reports near the view centre while the camera is low)
// and a continuous feed, newest first. It renders the `feed` view the news
// layer publishes and acts only through `onParams`.
@@ -75,9 +76,11 @@ const clear = () => props.onParams({ clear: true });
<section
v-if="view"
class="news-readout"
:aria-label="t('News, last 24 hours')"
:aria-label="t('News, last {hours} hours', { hours: view.windowHours })"
>
<p class="news-readout__window ui-mono">{{ t('Last 24 h') }}</p>
<p class="news-readout__window ui-mono">{{
t('Last {hours} h', { hours: view.windowHours })
}}</p>
<div class="news-readout__chips" role="group" :aria-label="t('Hotspots')">
<RailChips
:chips="chips"
@@ -136,7 +139,11 @@ const clear = () => props.onParams({ clear: true });
<UiEmpty
v-else-if="!view.inView.items.length && !view.inView.loading"
class="news-readout__empty"
:title="t('Nothing reported near here in the last 24 h')"
:title="
t('Nothing reported near here in the last {hours} h', {
hours: view.windowHours,
})
"
/>
<ol
v-else
@@ -173,7 +180,11 @@ const clear = () => props.onParams({ clear: true });
<UiEmpty
v-else-if="!view.loading && !view.error"
class="news-readout__empty"
:title="t('No news matches these filters in the last 24 h')"
:title="
t('No news matches these filters in the last {hours} h', {
hours: view.windowHours,
})
"
/>
<div class="news-readout__footer">
<span class="news-readout__status" role="status">{{ status }}</span>
+10
View File
@@ -76,6 +76,7 @@ function view(extra = {}) {
inView: { active: false, loading: false, error: null, items: [] },
selected: null,
hasMore: true,
windowHours: 24,
loading: false,
error: null,
...extra,
@@ -276,3 +277,12 @@ test('scrolling to the end asks once per page, again after a filter change or a
assert.equal(loadMores(), 3);
news.destroy();
});
test('the panel names the window the server serves', () => {
const { news, panel } = fixture();
news.update(view({ windowHours: 12 }));
assert.equal(
panel.querySelector('.news-readout__window').textContent,
'Last 12 h',
);
});