diff --git a/README.md b/README.md index f47f6f065..f70bd2f81 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,7 @@ Keep the `env`: bash treats `UID` as read-only, so a plain `UID=1026` in front o On SELinux hosts (Fedora, RHEL), compose mounts the checkout with `:z` so the container may read it; Docker may relabel the checkout for container access, which can take a moment on a large checkout. Without that, the start fails with `Cannot find module '/app/scripts/docker-start.mjs'`. -Behind a reverse proxy, put your keys in `.env` or pass them as environment variables - Provider Settings refuses proxied requests on purpose - and add the hostname you serve it on, for example `GEV_ALLOWED_HOSTS=globe.example.lan` (see [Sharing an instance](#-sharing-an-instance)). The map routes compare the browser's `Origin` with the `Host` the server sees, so the proxy must pass the original `Host` and, when it terminates TLS, `X-Forwarded-Proto`. Caddy does both by default; for nginx add `proxy_set_header Host $http_host;`, `proxy_set_header X-Forwarded-Proto $scheme;` and `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`. On an install outside Docker, that forwarding header also rate-limits the proxy's users apart from your own browser. Under the default Docker bridge network every client on the host, your browser included, reaches the container from its gateway address and shares one Overpass bucket. +Behind a reverse proxy, put your keys in `.env` or pass them as environment variables - Provider Settings and the News layer refuse proxied requests on purpose - and add the hostname you serve it on, for example `GEV_ALLOWED_HOSTS=globe.example.lan` (see [Sharing an instance](#-sharing-an-instance)). The map routes compare the browser's `Origin` with the `Host` the server sees, so the proxy must pass the original `Host` and, when it terminates TLS, `X-Forwarded-Proto`. Caddy does both by default; for nginx add `proxy_set_header Host $http_host;`, `proxy_set_header X-Forwarded-Proto $scheme;` and `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`. On an install outside Docker, that forwarding header also rate-limits the proxy's users apart from your own browser. Under the default Docker bridge network every client on the host, your browser included, reaches the container from its gateway address and shares one Overpass bucket. @@ -236,8 +236,8 @@ the container. ### Path 4 — GitHub Codespaces (cloud, no local install) -Codespaces requires a GitHub account. Overpass queries and OpenAI voice token -requests return HTTP 403 through its forwarded HTTPS URL, even with API keys +Codespaces requires a GitHub account. Overpass queries, the News layer and OpenAI +voice token requests return HTTP 403 through its forwarded HTTPS URL, even with API keys in `.env` and `GEV_ALLOWED_HOSTS` configured. Use a local setup for these features. The codespace runs the dev server in a cloud workspace, so Node and the dependencies diff --git a/changelog.d/1593-news-layer.md b/changelog.d/1593-news-layer.md index 748dab0bf..06d6a3632 100644 --- a/changelog.d/1593-news-layer.md +++ b/changelog.d/1593-news-layer.md @@ -23,7 +23,7 @@ - Cockpit Regional News holds its list to the last 24 hours, newest first, on the server and in the client, and labels its four states apart: fresh results with no banner and a `LAST 24 H` footer, earlier results kept after - a failed refresh as `CACHED · REFRESH FAILED`, `NO NEWS IN THE LAST 24 H`, + a failed refresh near the same place as `CACHED · REFRESH FAILED`, `NO NEWS IN THE LAST 24 H`, and `REGIONAL NEWS UNAVAILABLE`. The banner is now really hidden over a populated list, and the footer no longer reverts to a static `RECENT` on a page change (Denis Groshev, @ZloyS, #1593). diff --git a/docs/KNOWN-ISSUES.md b/docs/KNOWN-ISSUES.md index 6b2eaf8b0..5b246a73e 100644 --- a/docs/KNOWN-ISSUES.md +++ b/docs/KNOWN-ISSUES.md @@ -170,8 +170,9 @@ Status: Open (limits) and is tried again with the first ask a minute or more later. - Local needs a World News API key. Its free plan allows 50 points a day; Core stops at `WORLDNEWSAPI_DAILY_READS` reads (default 30) and Local then says the - daily limit is reached. A request that never reached World News API (offline, - timed out) is not counted; any answer from it is, a failing one too. + daily limit is reached. A request that never reached World News API (offline) + is not counted; any answer from it is, a failing one too, and so is a + request that timed out. - A country's news is kept for at most 1 hour (World News API's terms, which also ask the operator for its written permission to cache at all). While World News API cannot be read, Local in that country therefore reads diff --git a/src/layers/news/index.js b/src/layers/news/index.js index 7992884ce..65a653dd7 100644 --- a/src/layers/news/index.js +++ b/src/layers/news/index.js @@ -1083,6 +1083,7 @@ export function createNewsLayer({ minor: page.reports.filter((r) => withinNewsWindow(r, now(), maxWindowMs), ), + readAt: now(), loading: false, error: null, }; @@ -1094,7 +1095,8 @@ export function createNewsLayer({ ...inView, // A missing key is read again with the next refresh, not each pan. readKey: need ? readKey : inView.readKey, - minor: need ? [] : inView.minor, + // World News API's news is kept an hour at most, as the server does. + minor: need || now() - inView.readAt >= 3_600_000 ? [] : inView.minor, loading: false, error: need ? null : cause?.message || FEED_UNAVAILABLE, }; diff --git a/src/layers/news/layer.test.mjs b/src/layers/news/layer.test.mjs index 3d9b1db8f..0c0d60bf6 100644 --- a/src/layers/news/layer.test.mjs +++ b/src/layers/news/layer.test.mjs @@ -45,6 +45,7 @@ function fixture( reduced = false, centre = () => ({ lat: 49, lon: 36 }), credits = [], + now = () => NOW, } = {}, ) { const calls = { feed: [], hotspots: 0, outline: 0 }; @@ -110,7 +111,7 @@ function fixture( const layer = createNewsLayer({ source, overlayHost, - now: () => NOW, + now, matchMedia: () => ({ matches: reduced }), getViewCentre: () => centre(), registerCredit: (_viewer, credit) => { @@ -840,6 +841,43 @@ test('a failed Conflicts read leaves Local reading with each refresh', async (t) assert.equal(view.error, 'News feed unavailable'); }); +test('Local reports are listed an hour at most while they cannot be read again', async (t) => { + t.mock.timers.enable({ apis: ['setTimeout'] }); + let at = NOW; + let failing = false; + const { layer } = fixture(t, { + height: 1_000_000, + now: () => at, + source: { + getFeed: async (query) => { + if (query.at && failing) throw new Error('News feed unavailable'); + return { + reports: [report('inside', { relevance: 'minor' })], + nextBefore: null, + }; + }, + getAreaOutline: async () => ukraine, + }, + }); + const listed = () => + layer.getRowControls().feed.inView.items.map((item) => item.id); + layer.enable(); + await layer.update(); + layer.setParams({ filters: { section: 'local' } }); + await flush(); + assert.deepEqual(listed(), ['inside']); + // World News API's terms: its news is kept an hour at most, here too. + failing = true; + at += 59 * 60_000; + await layer.update(); + await flush(); + assert.deepEqual(listed(), ['inside']); + at += 2 * 60_000; + await layer.update(); + await flush(); + assert.deepEqual(listed(), []); +}); + test('a failed read keeps the layer on with an error; disable clears the overlay', async (t) => { const { layer, entries } = fixture(t, { source: { getFeed: async () => Promise.reject(new Error('boom')) },