openvibers/openvibe.food Commits

Food: Watch for changes on a food bank's own website (#47)

OP OpenVibers committed 29 days ago · df2051d55a · 2 files, +16 −2

The plan's composition for Food is Sources + Tools + AI + Watch, and Watch was the missing part. Food banks change their hours and close for holidays on their own websites, which OpenStreetMap rarely reflects.

- Each food-help result on `/near` (food bank, soup kitchen, community fridge, give box) that has a safe website now shows **Watch for changes** next to **Their website**.
- It opens OpenVibe.Watch's "a page changes" form, filled in with that website and the name `Food: <place>`. The person reviews and creates the watch on Watch, which then tells them (or runs their agent) when the page changes.
- Grocery stores get no link: a chain's site changes constantly.
- One muted line under the results explains what the link does.
- Only a URL that already passed `safeUrl` (http/https, checked at the source and again in the renderer) is ever prefilled.
- `test/places.test.js`: of four food banks with hostile, smuggled, `data:` and real websites, exactly one Watch link appears. It has template `page`, the real URL and the name, and the explanation is present.

Changes

ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex a7e0d2d..b62935c 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -67,6 +67,11 @@ ${panel ? html`<p class="food-search-note">Free, no account. Places from OpenStr
</form>`;
}
++ // OpenVibe.Watch's new-watch form, filled in to follow a food bank's own website for changes (new hours, a closure);
++ // the person reviews and creates it on Watch. Only for food help: a grocery chain's site changes all the time.
++ const WATCH_NEW = 'https://openvibe.watch/watches/new';
++ const watchHref = (place, website) => `${WATCH_NEW}?${new URLSearchParams({ template: 'page', url: website, name: `Food: ${place.name}`.slice(0, 80) })}`;
++
function placeRow(res_) {
// The source already drops a non-http(s) website from OSM (server/food/places.js); checked again here, so a
// hostile or stale value can never reach an href. Anything but a safe URL simply shows no website link.
@@@ -76,7 +81,7 @@ ${panel ? html`<p class="food-search-note">Free, no account. Places from OpenStr
<p class="muted small">${km(res_.distance_km)} away${res_.address ? html` · ${res_.address}` : ''}${res_.operator ? html` · run by ${res_.operator}` : ''}</p>
${res_.opening_hours ? html`<p class="small">Opening hours: ${res_.opening_hours}</p>` : ''}
${res_.phone ? html`<p class="small">Phone: <a href="tel:${String(res_.phone).replace(/[^\d+]/g, '')}">${res_.phone}</a></p>` : ''}
--<p class="small">${website ? html`<a href="${website}" rel="noopener nofollow">Their website</a> · ` : ''}<a href="${res_.osm_url}" rel="noopener nofollow">View on OpenStreetMap</a>${res_.wheelchair === true ? html` · wheelchair accessible` : ''}</p>
++<p class="small">${website ? html`<a href="${website}" rel="noopener nofollow">Their website</a> · ` : ''}${website && res_.kind === 'foodbank' ? html`<a href="${watchHref(res_, website)}">Watch for changes</a> · ` : ''}<a href="${res_.osm_url}" rel="noopener nofollow">View on OpenStreetMap</a>${res_.wheelchair === true ? html` · wheelchair accessible` : ''}</p>
</li>`;
}
@@@ -84,7 +89,9 @@ ${res_.phone ? html`<p class="small">Phone: <a href="tel:${String(res_.phone).re
if (!out.results.length) {
return html`<p class="muted">Nothing was found within ${out.radius_km} km. OpenStreetMap does not know every food bank — try a wider area, or ask your local council.</p>`;
}
-- return html`<ul class="places">${out.results.map(placeRow)}</ul>`;
++ const watchable = out.results.some((r) => r.kind === 'foodbank' && safeUrl(r.website));
++ return html`<ul class="places">${out.results.map(placeRow)}</ul>
++${watchable ? html`<p class="muted small">Watch for changes opens OpenVibe.Watch with that place's website filled in: it checks the page and tells you when it changes, such as new opening hours or a closure.</p>` : ''}`;
}
// ── Home ─────────────────────────────────────────────────
ddiff --git a/test/places.test.js b/test/places.test.js
iindex d154065..66dd6e0 100644
---- a/test/places.test.js
++++ b/test/places.test.js
@@@ -168,6 +168,13 @@ const OVERPASS = '/api/interpreter';
assert.ok(!/href="data:text\/html/i.test(page.text), 'no data: href from OSM reaches the page');
assert.match(page.text, /href="https:\/\/example\.org\/pantry"/, 'the real website is linked');
assert.match(page.text, /Their website/);
++ // Watch for changes: OpenVibe.Watch's page template, filled in with the safe website only.
++ const watch = [...page.text.matchAll(/href="(https:\/\/openvibe\.watch\/watches\/new\?[^"]+)"/g)].map((m) => new URL(m[1].replace(/&amp;/g, '&')));
++ assert.strictEqual(watch.length, 1, 'one Watch link: only the place with a safe website');
++ assert.strictEqual(watch[0].searchParams.get('template'), 'page');
++ assert.strictEqual(watch[0].searchParams.get('url'), 'https://example.org/pantry');
++ assert.strictEqual(watch[0].searchParams.get('name'), 'Food: Real Website');
++ assert.match(page.text, /Watch for changes opens OpenVibe\.Watch/);
});
await check('public reads need no token, and bad input is refused with a stable code', async () => {