openvibers/openvibe.tools Commits
Tools: food.openvibe.tools sends people to OpenVibe.Food (301); the food finder API stays (#72)
Changes
@@@ -56,7 +56,7 @@ apps/├── gateway # apex (server-rendered index, family + tool pages, catalog.json), the tool registry│ # and host roles, Net.OpenVibe + Dev.OpenVibe tools, proxy to the satellites├── maps # Maps.OpenVibe — survival map for North America--├── food # Food.OpenVibe — grocery / food bank finder (proxies maps backend)++├── food # the food finder API (proxies maps backend); its page moved to OpenVibe.Food (301 to openvibe.food)├── img # Img.OpenVibe — image converter & processing tools├── yt # YT.OpenVibe — YouTube downloader├── audio # Audio.OpenVibe — audio converter & effects@@@ -339,9 +339,9 @@<span class="cross-icon"><i class="fa-solid fa-map-location-dot"></i></span><span class="cross-info"><span class="cross-name">Maps.OpenVibe</span><span class="cross-desc">Camps, resources & shelters</span></span></a>-- <a class="cross-card" href="https://food.openvibe.tools">++ <a class="cross-card" href="https://openvibe.food"><span class="cross-icon"><i class="fa-solid fa-utensils"></i></span>-- <span class="cross-info"><span class="cross-name">Food.OpenVibe</span><span class="cross-desc">Food banks & meal planner</span></span>++ <span class="cross-info"><span class="cross-name">OpenVibe.Food</span><span class="cross-desc">Food banks, cheap groceries & meal plans</span></span></a></div></section>@@@ -1,6 +1,9 @@/**-- * food.openvibe.tools — Lightweight frontend server-- * Proxies food API calls to openvibe-maps backend (port 4010)++ * food.openvibe.tools — the food finder's API (proxied to the openvibe-maps backend, port 4010).++ *++ * The page moved to OpenVibe.Food (openvibe.food: food banks and budget grocers near you, the food list, meal plans,++ * pantry, recipes): a person opening food.openvibe.tools is sent there with a 301. The JSON routes stay, as every++ * tool's do (the open tools registry, ADR-027).*/require('dotenv').config();const express = require('express');@@@ -14,7 +17,7 @@ const cookieParser = require('cookie-parser');// ── Analytics ────────────────────────────────────────────────const { AnalyticsTrackerPg } = require('openvibe-shared/analytics/pg'); // ADR-021: no IP/user id, route templates, raw rows pruned after 30 days, Sec-GPC/DNT not recordedconst { requireInternalAccess } = require('../../_shared/internal-token');--const { hostGuard, stampedPage } = require('../../_shared/host-role');++const { hostGuard } = require('../../_shared/host-role');const { createToolsApi, exceptRegistry } = require('../../_shared/tools/http');const { createLocalRegistry, requiresStatus } = require('../../_shared/tools/local');const { createGuard, TRUST_PROXY } = require('../../_shared/guard');@@@ -116,8 +119,10 @@ app.use(analytics.middleware());// page's canonical / og:url / JSON-LD follow it; aliases are redirected; other hosts go to the tools index.const HOST = 'food.openvibe.tools';app.use(hostGuard({ knows: (h) => h === HOST }));--const sendIndex = stampedPage(path.join(__dirname, '..', 'public', 'index.html'), HOST);--app.get(['/', '/index.html'], sendIndex);++// The page is OpenVibe.Food's now; the JSON routes below stay for whoever calls them.++const FOOD_URL = (process.env.OV_FOOD_PUBLIC_URL || 'https://openvibe.food').replace(/\/+$/, '');++const toFood = (req, res) => res.redirect(301, `${FOOD_URL}/`);++app.get(['/', '/index.html'], toFood);// Static filesapp.use(express.static(path.join(__dirname, '..', 'public'), { maxAge: '1d', index: false }));@@@ -163,8 +168,8 @@ app.get('/api/internal/analytics/bots', internalAccess, async (req, res) => {catch (err) { res.status(500).json({ ok: false, error: err.message }); }});--// SPA fallback--app.get('*', (req, res) => sendIndex(req, res));++// What used to be the page's own paths go to OpenVibe.Food as well (the API answers JSON 404s).++app.get('*', (req, res) => (req.path.startsWith('/api/') ? res.status(404).json({ error: 'Not found' }) : toFood(req, res)));const server = app.listen(PORT, '127.0.0.1', () => {console.log(`[Food.OpenVibe] 🍽️ food.openvibe.tools listening on 127.0.0.1:${PORT}`);@@@ -20,6 +20,14 @@ const { startApp } = require('../../_shared/test/spawn');const other = await ask('203.0.113.2');assert.strictEqual(other.status, 200, 'a second visitor has their own bucket');++ // The page moved to OpenVibe.Food: a person is sent there; the JSON routes stay.++ for (const p of ['/', '/index.html', '/food-banks']) {++ const r = await fetch(`${food.base}${p}`, { redirect: 'manual', headers: { Host: 'food.openvibe.tools' } });++ assert.strictEqual(r.status, 301, p);++ assert.strictEqual(r.headers.get('location'), 'https://openvibe.food/', p);++ }++ assert.strictEqual((await fetch(`${food.base}/api/nothing-here`)).status, 404, 'an unknown API path is a JSON 404, not a redirect');++const src = (app) => fs.readFileSync(path.join(__dirname, '..', '..', app, 'server', 'index.js'), 'utf8');assert.match(src('maps'), /app\.set\('trust proxy', TRUST_PROXY\)/, 'maps trusts one forwarded hop, on loopback only');assert.match(src('food'), /app\.set\('trust proxy', TRUST_PROXY\)/);@@@ -28,5 +36,5 @@ const { startApp } = require('../../_shared/test/spawn');await food.kill();await maps.kill();}-- console.log('food → maps forwards the visitor address; maps limits per person: all checks passed');++ console.log('food → maps forwards the visitor address; maps limits per person; the page redirects to openvibe.food: all checks passed');})().catch((err) => { console.error(err); process.exit(1); });@@@ -90,7 +90,7 @@ const FAMILIES = [{id: 'places', name: 'Maps & Food', icon: 'map', path: '/maps-and-food', hub: null, hubAliases: [], port: null,tagline: 'Find water, shelter, supplies and somewhere to eat',-- description: 'Two practical maps. The survival map shows drinking water, public toilets, shelters, hospitals, fuel and other essentials near any place on earth. The food finder shows restaurants, cafes, markets and food banks around you.',++ description: 'Two practical maps. The survival map shows drinking water, public toilets, shelters, hospitals, fuel and other essentials near any place on earth. The food finder (now OpenVibe.Food, at openvibe.food) shows food banks, budget grocers and markets around you, with meal plans and a pantry.',intro: 'Both tools use open map data. Allow location access or search for a town, then filter the map to the kind of place you need.',faq: [['Where does the map data come from?', 'OpenStreetMap and other open datasets, maintained by volunteers around the world. Coverage is best in towns and cities.'],@@@ -416,7 +416,7 @@service: 'maps', variant: 'compact', mount: '#ov-footer', apiBase: 'https://openvibe.network',links: [{ heading: 'Maps.OpenVibe', items: [{ name: 'Survival map', url: 'https://maps.openvibe.tools' },-- { name: 'Food.OpenVibe', url: 'https://food.openvibe.tools' },++ { name: 'OpenVibe.Food', url: 'https://openvibe.food' },{ name: 'All tools', url: 'https://openvibe.tools' },] }],});
@@ -56,7 +56,7 @@ apps/
@@ -56,7 +56,7 @@ apps/
├── gateway # apex (server-rendered index, family + tool pages, catalog.json), the tool registry
├── gateway # apex (server-rendered index, family + tool pages, catalog.json), the tool registry
│ # and host roles, Net.OpenVibe + Dev.OpenVibe tools, proxy to the satellites
│ # and host roles, Net.OpenVibe + Dev.OpenVibe tools, proxy to the satellites
├── maps # Maps.OpenVibe — survival map for North America
├── maps # Maps.OpenVibe — survival map for North America
-├── food # Food.OpenVibe — grocery / food bank finder (proxies maps backend)
+├── food # the food finder API (proxies maps backend); its page moved to OpenVibe.Food (301 to openvibe.food)
├── img # Img.OpenVibe — image converter & processing tools
├── img # Img.OpenVibe — image converter & processing tools
├── yt # YT.OpenVibe — YouTube downloader
├── yt # YT.OpenVibe — YouTube downloader
├── audio # Audio.OpenVibe — audio converter & effects
├── audio # Audio.OpenVibe — audio converter & effects
@@ -339,9 +339,9 @@
@@ -339,9 +339,9 @@
<span class="cross-icon"><i class="fa-solid fa-map-location-dot"></i></span>
<span class="cross-icon"><i class="fa-solid fa-map-location-dot"></i></span>
<span class="cross-info"><span class="cross-name">Maps.OpenVibe</span><span class="cross-desc">Camps, resources & shelters</span></span>
<span class="cross-info"><span class="cross-name">Maps.OpenVibe</span><span class="cross-desc">Camps, resources & shelters</span></span>
</a>
</a>
- <a class="cross-card" href="https://food.openvibe.tools">
+ <a class="cross-card" href="https://openvibe.food">
<span class="cross-icon"><i class="fa-solid fa-utensils"></i></span>
<span class="cross-icon"><i class="fa-solid fa-utensils"></i></span>
- <span class="cross-info"><span class="cross-name">Food.OpenVibe</span><span class="cross-desc">Food banks & meal planner</span></span>
+ <span class="cross-info"><span class="cross-name">OpenVibe.Food</span><span class="cross-desc">Food banks, cheap groceries & meal plans</span></span>
</a>
</a>
</div>
</div>
</section>
</section>
@@ -1,6 +1,9 @@
@@ -1,6 +1,9 @@
/**
/**
- * food.openvibe.tools — Lightweight frontend server
- * Proxies food API calls to openvibe-maps backend (port 4010)
+ * food.openvibe.tools — the food finder's API (proxied to the openvibe-maps backend, port 4010).
+ *
+ * The page moved to OpenVibe.Food (openvibe.food: food banks and budget grocers near you, the food list, meal plans,
+ * pantry, recipes): a person opening food.openvibe.tools is sent there with a 301. The JSON routes stay, as every
+ * tool's do (the open tools registry, ADR-027).
*/
*/
require('dotenv').config();
require('dotenv').config();
const express = require('express');
const express = require('express');
@@ -14,7 +17,7 @@ const cookieParser = require('cookie-parser');
@@ -14,7 +17,7 @@ const cookieParser = require('cookie-parser');
// ── Analytics ────────────────────────────────────────────────
// ── Analytics ────────────────────────────────────────────────
const { AnalyticsTrackerPg } = require('openvibe-shared/analytics/pg'); // ADR-021: no IP/user id, route templates, raw rows pruned after 30 days, Sec-GPC/DNT not recorded
const { AnalyticsTrackerPg } = require('openvibe-shared/analytics/pg'); // ADR-021: no IP/user id, route templates, raw rows pruned after 30 days, Sec-GPC/DNT not recorded
const { requireInternalAccess } = require('../../_shared/internal-token');
const { requireInternalAccess } = require('../../_shared/internal-token');
-const { hostGuard, stampedPage } = require('../../_shared/host-role');
+const { hostGuard } = require('../../_shared/host-role');
const { createToolsApi, exceptRegistry } = require('../../_shared/tools/http');
const { createToolsApi, exceptRegistry } = require('../../_shared/tools/http');
const { createLocalRegistry, requiresStatus } = require('../../_shared/tools/local');
const { createLocalRegistry, requiresStatus } = require('../../_shared/tools/local');
const { createGuard, TRUST_PROXY } = require('../../_shared/guard');
const { createGuard, TRUST_PROXY } = require('../../_shared/guard');
@@ -116,8 +119,10 @@ app.use(analytics.middleware());
@@ -116,8 +119,10 @@ app.use(analytics.middleware());
// page's canonical / og:url / JSON-LD follow it; aliases are redirected; other hosts go to the tools index.
// page's canonical / og:url / JSON-LD follow it; aliases are redirected; other hosts go to the tools index.
const HOST = 'food.openvibe.tools';
const HOST = 'food.openvibe.tools';
app.use(hostGuard({ knows: (h) => h === HOST }));
app.use(hostGuard({ knows: (h) => h === HOST }));
-const sendIndex = stampedPage(path.join(__dirname, '..', 'public', 'index.html'), HOST);
-app.get(['/', '/index.html'], sendIndex);
+// The page is OpenVibe.Food's now; the JSON routes below stay for whoever calls them.
+const FOOD_URL = (process.env.OV_FOOD_PUBLIC_URL || 'https://openvibe.food').replace(/\/+$/, '');
+const toFood = (req, res) => res.redirect(301, `${FOOD_URL}/`);
+app.get(['/', '/index.html'], toFood);
// Static files
// Static files
app.use(express.static(path.join(__dirname, '..', 'public'), { maxAge: '1d', index: false }));
app.use(express.static(path.join(__dirname, '..', 'public'), { maxAge: '1d', index: false }));
@@ -163,8 +168,8 @@ app.get('/api/internal/analytics/bots', internalAccess, async (req, res) => {
@@ -163,8 +168,8 @@ app.get('/api/internal/analytics/bots', internalAccess, async (req, res) => {
catch (err) { res.status(500).json({ ok: false, error: err.message }); }
catch (err) { res.status(500).json({ ok: false, error: err.message }); }
});
});
-// SPA fallback
-app.get('*', (req, res) => sendIndex(req, res));
+// What used to be the page's own paths go to OpenVibe.Food as well (the API answers JSON 404s).
+app.get('*', (req, res) => (req.path.startsWith('/api/') ? res.status(404).json({ error: 'Not found' }) : toFood(req, res)));
const server = app.listen(PORT, '127.0.0.1', () => {
const server = app.listen(PORT, '127.0.0.1', () => {
console.log(`[Food.OpenVibe] 🍽️ food.openvibe.tools listening on 127.0.0.1:${PORT}`);
console.log(`[Food.OpenVibe] 🍽️ food.openvibe.tools listening on 127.0.0.1:${PORT}`);
@@ -20,6 +20,14 @@ const { startApp } = require('../../_shared/test/spawn');
@@ -20,6 +20,14 @@ const { startApp } = require('../../_shared/test/spawn');
const other = await ask('203.0.113.2');
const other = await ask('203.0.113.2');
assert.strictEqual(other.status, 200, 'a second visitor has their own bucket');
assert.strictEqual(other.status, 200, 'a second visitor has their own bucket');
+ // The page moved to OpenVibe.Food: a person is sent there; the JSON routes stay.
+ for (const p of ['/', '/index.html', '/food-banks']) {
+ const r = await fetch(`${food.base}${p}`, { redirect: 'manual', headers: { Host: 'food.openvibe.tools' } });
+ assert.strictEqual(r.status, 301, p);
+ assert.strictEqual(r.headers.get('location'), 'https://openvibe.food/', p);
+ }
+ assert.strictEqual((await fetch(`${food.base}/api/nothing-here`)).status, 404, 'an unknown API path is a JSON 404, not a redirect');
+
const src = (app) => fs.readFileSync(path.join(__dirname, '..', '..', app, 'server', 'index.js'), 'utf8');
const src = (app) => fs.readFileSync(path.join(__dirname, '..', '..', app, 'server', 'index.js'), 'utf8');
assert.match(src('maps'), /app\.set\('trust proxy', TRUST_PROXY\)/, 'maps trusts one forwarded hop, on loopback only');
assert.match(src('maps'), /app\.set\('trust proxy', TRUST_PROXY\)/, 'maps trusts one forwarded hop, on loopback only');
assert.match(src('food'), /app\.set\('trust proxy', TRUST_PROXY\)/);
assert.match(src('food'), /app\.set\('trust proxy', TRUST_PROXY\)/);
@@ -28,5 +36,5 @@ const { startApp } = require('../../_shared/test/spawn');
@@ -28,5 +36,5 @@ const { startApp } = require('../../_shared/test/spawn');
await food.kill();
await food.kill();
await maps.kill();
await maps.kill();
}
}
- console.log('food → maps forwards the visitor address; maps limits per person: all checks passed');
+ console.log('food → maps forwards the visitor address; maps limits per person; the page redirects to openvibe.food: all checks passed');
})().catch((err) => { console.error(err); process.exit(1); });
})().catch((err) => { console.error(err); process.exit(1); });
@@ -90,7 +90,7 @@ const FAMILIES = [
@@ -90,7 +90,7 @@ const FAMILIES = [
{
{
id: 'places', name: 'Maps & Food', icon: 'map', path: '/maps-and-food', hub: null, hubAliases: [], port: null,
id: 'places', name: 'Maps & Food', icon: 'map', path: '/maps-and-food', hub: null, hubAliases: [], port: null,
tagline: 'Find water, shelter, supplies and somewhere to eat',
tagline: 'Find water, shelter, supplies and somewhere to eat',
- description: 'Two practical maps. The survival map shows drinking water, public toilets, shelters, hospitals, fuel and other essentials near any place on earth. The food finder shows restaurants, cafes, markets and food banks around you.',
+ description: 'Two practical maps. The survival map shows drinking water, public toilets, shelters, hospitals, fuel and other essentials near any place on earth. The food finder (now OpenVibe.Food, at openvibe.food) shows food banks, budget grocers and markets around you, with meal plans and a pantry.',
intro: 'Both tools use open map data. Allow location access or search for a town, then filter the map to the kind of place you need.',
intro: 'Both tools use open map data. Allow location access or search for a town, then filter the map to the kind of place you need.',
faq: [
faq: [
['Where does the map data come from?', 'OpenStreetMap and other open datasets, maintained by volunteers around the world. Coverage is best in towns and cities.'],
['Where does the map data come from?', 'OpenStreetMap and other open datasets, maintained by volunteers around the world. Coverage is best in towns and cities.'],
@@ -416,7 +416,7 @@
@@ -416,7 +416,7 @@
service: 'maps', variant: 'compact', mount: '#ov-footer', apiBase: 'https://openvibe.network',
service: 'maps', variant: 'compact', mount: '#ov-footer', apiBase: 'https://openvibe.network',
links: [{ heading: 'Maps.OpenVibe', items: [
links: [{ heading: 'Maps.OpenVibe', items: [
{ name: 'Survival map', url: 'https://maps.openvibe.tools' },
{ name: 'Survival map', url: 'https://maps.openvibe.tools' },
- { name: 'Food.OpenVibe', url: 'https://food.openvibe.tools' },
+ { name: 'OpenVibe.Food', url: 'https://openvibe.food' },
{ name: 'All tools', url: 'https://openvibe.tools' },
{ name: 'All tools', url: 'https://openvibe.tools' },
] }],
] }],
});
});