mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 05:42:22 +00:00
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:
1 parent
f3e206bf37
commit
60fad38c3f
18 files changed
+178
-73
No files matched your search
@@ -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
|
||||
|
||||
@@ -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": "اللون: الخطورة · الرمز: نوع الحدث",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "色: 深刻度 · アイコン: 出来事の種類",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Цвет: тяжесть · значок: тип события",
|
||||
|
||||
@@ -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": "Колір: тяжкість · значок: тип події",
|
||||
|
||||
@@ -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": "颜色:严重程度 · 图标:事件类型",
|
||||
|
||||
@@ -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": "顏色:嚴重程度 · 圖示:事件類型",
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user