diff --git a/.github/workflows/validate-datasets.yml b/.github/workflows/validate-datasets.yml index e2d2b1c..29a7b2e 100644 --- a/.github/workflows/validate-datasets.yml +++ b/.github/workflows/validate-datasets.yml @@ -32,6 +32,10 @@ jobs: - name: Validate datasets offline run: npm run validate-datasets:offline + - name: Check reviewed app source snapshots + if: github.event_name == 'schedule' + run: npx vitest run src/content/apps.test.ts + - name: Validate provider contracts if: github.event_name != 'pull_request' run: npm run validate-providers diff --git a/README.md b/README.md index 72b5bce..dd64ca4 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,11 @@ Each dataset includes searchable metadata, authoritative source links, practical access instructions, a copyable Python example, and a small project for testing a useful product signal. +The Apps directory showcases six data-powered projects. Each app page cites the +external sources behind it, links to matching dataset guides where available, +and links to its hosted experience or archive and source code. The projects run +outside this static site. + The catalog optimizes for beginner clarity, contribution simplicity, and dataset quality — not catalog size. @@ -59,19 +64,23 @@ interaction rules, and checksums. - Azure `#5858C8` links and light-surface focus; amber `#FF9940` dark-surface focus - Roboto typography, 10px controls, 16px cards, and 44px minimum interactive targets -Data renders only its local header (Datasets, Build Paths, Contribute), with +Data renders only its local header (Datasets, Apps, Build Paths, Contribute), with sticky offsets measured from its actual height. The shared Foundation footer remains. Run `npm run check:design` when updating the vendored contract. ## Editing content -Content has two explicit sources of truth: +Content has three explicit sources of truth: - Shared application, navigation, accessibility, and SEO copy lives in [`src/content/site-copy.ts`](src/content/site-copy.ts). Non-description text uses Chicago Title Case; descriptions and other body copy stay in sentence case. - Dataset descriptions and beginner guides live with their metadata in the corresponding [`data/datasets/*.yaml`](data/datasets) file. +- App descriptions, availability, outbound links, and grouped source citations + live in [`src/content/apps`](src/content/apps). Each reviewed snapshot records + its product revision and review date. Review these at least every 90 days and + when a project changes; a listed source is not a claim that its feed is live. Each dataset selects one broad catalog theme. Domain and task tags are normalized through [`data/vocabulary.yaml`](data/vocabulary.yaml): aliases diff --git a/e2e/accessibility.spec.ts b/e2e/accessibility.spec.ts index 15daf30..c1984c4 100644 --- a/e2e/accessibility.spec.ts +++ b/e2e/accessibility.spec.ts @@ -44,3 +44,11 @@ test("collection, theme, and contribute pages pass accessibility checks", async await expectNoAccessibilityViolations(page); } }); + +test("Apps listing, long source map, and archive page pass accessibility checks", async ({ page }) => { + for (const path of ["/apps", "/apps/travelcanary", "/apps/stackingsats"]) { + await page.goto(path); + await expect(page.locator("main h1")).toBeVisible(); + await expectNoAccessibilityViolations(page); + } +}); diff --git a/e2e/apps.spec.ts b/e2e/apps.spec.ts new file mode 100644 index 0000000..f8979d6 --- /dev/null +++ b/e2e/apps.spec.ts @@ -0,0 +1,110 @@ +import { existsSync, readdirSync } from "node:fs"; +import { expect, test } from "@playwright/test"; + +const appNames = [ + ["titanskies", "TitanSkies"], + ["hyperoptions", "HyperOptions"], + ["travelcanary", "TravelCanary"], + ["househunter", "HouseHunter"], + ["rockyroad", "RockyRoad"], + ["stackingsats", "StackingSats"], +] as const; + +test("the seven Apps routes are exported and show the right project", async ({ page, request }) => { + expect(existsSync("out/apps.html")).toBe(true); + const listing = await request.get("/apps"); + expect(listing.ok()).toBe(true); + expect(readdirSync("out/apps").filter((file) => file.endsWith(".html")).sort()).toEqual( + appNames.map(([slug]) => `${slug}.html`).sort(), + ); + expect((await request.get("/apps/not-a-project")).status()).toBe(404); + await page.goto("/apps"); + await expect(page.getByRole("heading", { level: 1, name: "Apps" })).toBeVisible(); + const cards = page.getByRole("article"); + await expect(cards).toHaveCount(6); + for (const [index, [slug, title]] of appNames.entries()) { + await expect(cards.nth(index).getByRole("heading", { level: 2, name: title })).toBeVisible(); + await expect(cards.nth(index).getByRole("link", { name: title, exact: true })).toHaveAttribute("href", `/apps/${slug}`); + } + for (const [slug, title] of appNames) { + expect(existsSync(`out/apps/${slug}.html`), slug).toBe(true); + const response = await request.get(`/apps/${slug}`); + expect(response.ok(), slug).toBe(true); + await page.goto(`/apps/${slug}`); + await expect(page.getByRole("heading", { level: 1, name: title })).toBeVisible(); + await expect(page.getByRole("navigation", { name: "Data navigation" }).getByRole("link", { name: "Apps", exact: true })).toHaveAttribute("aria-current", "page"); + await expect(page.getByRole("heading", { level: 2, name: "Data Sources" })).toBeVisible(); + await expect(page.getByRole("link", { name: "Apps", exact: true }).last()).toHaveAttribute("href", "/apps"); + await expect(page.getByRole("link", { name: /Official Source/ }).first()).toBeVisible(); + await expect(page.getByText("Role:", { exact: true }).first()).toBeVisible(); + await expect(page.getByText("Availability:", { exact: true }).first()).toBeVisible(); + const guidePaths = await page.locator('main a[href^="/datasets/"]').evaluateAll((links) => + [...new Set(links.map((link) => link.getAttribute("href")).filter((href): href is string => Boolean(href)))], + ); + for (const path of guidePaths) expect((await request.get(path)).ok(), `${slug}: ${path}`).toBe(true); + } +}); + +test("Apps is a primary header destination on listing and details", async ({ page }) => { + for (const path of ["/apps", "/apps/travelcanary", "/apps/stackingsats/"]) { + await page.goto(path); + const nav = page.getByRole("navigation", { name: "Data navigation" }); + await expect(nav.locator("a")).toHaveText(["Data", "Datasets", "Apps", "Build Paths", "Contribute"]); + await expect(nav.getByRole("link", { name: "Apps", exact: true })).toHaveAttribute("aria-current", "page"); + await expect(nav.getByRole("link", { name: "Datasets", exact: true })).not.toHaveAttribute("aria-current"); + } +}); + +test("cards and details expose only the intended external actions", async ({ page }) => { + await page.goto("/apps"); + const cards = page.getByRole("article"); + const expected = [ + ["Live App", "https://www.titanskies.com/", "https://github.com/hypertrial/titanskies"], + [undefined, undefined, "https://github.com/hypertrial/hyperoptions"], + ["Live App", "https://travelcanary.org/", "https://github.com/hypertrial/travelcanary"], + [undefined, undefined, "https://github.com/hypertrial/househunter"], + [undefined, undefined, "https://github.com/hypertrial/rockyroad"], + ["Archive", "https://stackingsats.org/", "https://github.com/hypertrial/stacksats"], + ] as const; + const statuses = [undefined, undefined, "Beta", "Alpha", "Alpha", "Archived"] as const; + for (const [index, [action, destination, source]] of expected.entries()) { + const card = cards.nth(index); + const status = statuses[index]; + if (status) await expect(card.getByText(status, { exact: true })).toBeVisible(); + else await expect(card.getByText(/^(Alpha|Beta|Archived)$/)).toHaveCount(0); + const links = card.locator('a[target="_blank"]'); + await expect(links).toHaveCount(action ? 2 : 1); + if (action) await expect(links.first()).toHaveAttribute("href", destination!); + await expect(links.last()).toHaveAttribute("href", source); + for (const link of await links.all()) { + await expect(link).toHaveAttribute("rel", /noopener.*noreferrer/); + await expect(link).toHaveAccessibleName(/opens in a new tab/); + } + } + await expect(page.getByText("Paid Target")).toHaveCount(0); + await cards.first().getByRole("link", { name: "TitanSkies", exact: true }).focus(); + await page.keyboard.press("Tab"); + await expect(cards.first().getByRole("link", { name: /Explore App/ })).toBeFocused(); + await page.keyboard.press("Tab"); + await expect(cards.first().getByRole("link", { name: /Live App/ })).toBeFocused(); + + await page.goto("/apps/stackingsats"); + await expect(page.getByRole("link", { name: /Archive/ })).toHaveAttribute("href", "https://stackingsats.org/"); + await expect(page.getByRole("link", { name: /Live App/ })).toHaveCount(0); + await expect(page.getByText(/historical|archiv/i).first()).toBeVisible(); +}); + +test("source links distinguish matching guides from official sources", async ({ page }) => { + await page.goto("/apps/travelcanary"); + const earthquake = page.getByRole("listitem").filter({ has: page.getByRole("heading", { name: "USGS earthquake feeds" }) }); + await expect(earthquake.getByRole("link", { name: /Dataset Guide/ })).toHaveAttribute("href", "/datasets/usgs-earthquakes"); + await expect(earthquake.getByRole("link", { name: /Official Source/ })).toHaveAttribute("href", "https://earthquake.usgs.gov/"); + await expect(earthquake.getByRole("link", { name: /Official Source/ })).toHaveAttribute("target", "_blank"); + await expect(earthquake.getByRole("link", { name: /Official Source/ })).toHaveAccessibleName(/opens in a new tab/); + await expect(page.getByText(/reviewed source inventory, not a live feed-health report/i)).toBeVisible(); + + await page.goto("/apps/househunter"); + const nri = page.getByRole("listitem").filter({ has: page.getByRole("heading", { name: "FEMA National Risk Index" }) }); + await expect(nri.getByRole("link", { name: /Dataset Guide/ })).toHaveCount(0); + await expect(nri).toContainText("FEMA National Flood Hazard Layer is a different dataset"); +}); diff --git a/e2e/design-experience.spec.ts b/e2e/design-experience.spec.ts index fd82a82..0332cb4 100644 --- a/e2e/design-experience.spec.ts +++ b/e2e/design-experience.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; -const families = ['/', '/collections', '/collections/first-builds', '/themes/environment-hazards', '/datasets/usgs-earthquakes', '/datasets/nasa-firms', '/contribute', '/404']; +const families = ['/', '/apps', '/collections', '/collections/first-builds', '/themes/environment-hazards', '/datasets/usgs-earthquakes', '/datasets/nasa-firms', '/contribute', '/404']; for (const width of [320, 390, 768, 1023, 1024, 1280, 1536]) { test(`shared surfaces reflow at ${width}px`, async ({ page, browserName }, info) => { test.skip(browserName !== 'chromium' && ![390, 1280].includes(width)); @@ -25,6 +25,24 @@ test('200% text and reduced motion retain search and reading context', async ({ } }); +test('TravelCanary source list remains readable at narrow and desktop widths and 200% text', async ({ page, browserName }) => { + test.skip(browserName !== 'chromium'); + for (const width of [320, 390, 1280]) { + await page.setViewportSize({ width, height: 900 }); + await page.goto('/apps/travelcanary'); + const sources = page.locator('main section[aria-labelledby="app-sources-title"] li'); + expect(await sources.count()).toBeGreaterThan(20); + await sources.last().scrollIntoViewIfNeeded(); + await expect(sources.last()).toBeVisible(); + await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); + if (width === 390) { + await page.addStyleTag({ content: 'html { font-size: 200% !important; }' }); + await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); + await expect(page.getByRole('heading', { level: 1, name: 'TravelCanary' })).toBeVisible(); + } + } +}); + test('guide anchors use the shared shell offset without an extra fixed-header gap', async ({ page }) => { await page.emulateMedia({ reducedMotion: 'reduce' }); for (const viewport of [{ width: 390, height: 844 }, { width: 844, height: 390 }, { width: 1280, height: 800 }]) { diff --git a/e2e/metadata.spec.ts b/e2e/metadata.spec.ts index bc4ea81..c8febd7 100644 --- a/e2e/metadata.spec.ts +++ b/e2e/metadata.spec.ts @@ -4,6 +4,15 @@ import { getActiveDatasets } from "../src/lib/datasets"; import { getAllCollections } from "../src/lib/collections"; import { DATASET_THEMES } from "../src/lib/schema"; +const appNames = [ + ["titanskies", "TitanSkies"], + ["hyperoptions", "HyperOptions"], + ["travelcanary", "TravelCanary"], + ["househunter", "HouseHunter"], + ["rockyroad", "RockyRoad"], + ["stackingsats", "StackingSats"], +] as const; + test("public routes expose canonical and social metadata", async ({ page }) => { await page.goto("/"); await expect(page.locator('meta[name="referrer"]')).toHaveAttribute( @@ -81,6 +90,17 @@ test("dataset guides expose canonical metadata and valid JSON-LD", async ({ ); }); +test("Apps listing and each project have their own canonical and social metadata", async ({ page }) => { + for (const [path, title] of [["/apps", "Apps"], ...appNames.map(([slug, name]) => [`/apps/${slug}`, name])] as const) { + await page.goto(path); + const canonical = `https://data.trilemma.foundation${path}`; + await expect(page.locator('link[rel="canonical"]')).toHaveAttribute("href", canonical); + await expect(page.locator('meta[property="og:url"]')).toHaveAttribute("content", canonical); + await expect(page.locator('meta[property="og:title"]')).toHaveAttribute("content", title); + await expect(page.locator('meta[name="description"]')).toHaveAttribute("content", /\S/); + } +}); + test("robots and sitemap enumerate the public static application", async ({ request, }) => { @@ -96,6 +116,10 @@ test("robots and sitemap enumerate the public static application", async ({ expect(body).toContain("https://data.trilemma.foundation"); expect(body).toContain("https://data.trilemma.foundation/collections"); expect(body).toContain("https://data.trilemma.foundation/contribute"); + expect(body).toContain("https://data.trilemma.foundation/apps"); + for (const [slug] of appNames) { + expect(body).toContain(`https://data.trilemma.foundation/apps/${slug}`); + } expect(body).not.toContain("https://data.trilemma.foundation/compare"); const datasets = getActiveDatasets(); for (const dataset of datasets) { @@ -105,5 +129,5 @@ test("robots and sitemap enumerate the public static application", async ({ } const extra = 1 + 1 + 1 + DATASET_THEMES.length + getAllCollections().length; - expect(body.match(//g)).toHaveLength(datasets.length + extra); + expect(body.match(//g)).toHaveLength(datasets.length + extra + 7); }); diff --git a/src/app/apps/[slug]/page.tsx b/src/app/apps/[slug]/page.tsx new file mode 100644 index 0000000..b5063ab --- /dev/null +++ b/src/app/apps/[slug]/page.tsx @@ -0,0 +1,94 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { AppActions } from "@/components/AppActions"; +import { Badge } from "@/components/ui/badge"; +import { apps, getAppBySlug, type AppSource } from "@/content/apps"; +import { appsCopy } from "@/content/site-copy"; +import { appPath, APPS_PATH, datasetPath, pageSocialMetadata } from "@/lib/seo"; + +export function generateStaticParams() { + return apps.map((app) => ({ slug: app.slug })); +} + +export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise { + const { slug } = await params; + const app = getAppBySlug(slug); + if (!app) return { title: "Page Not Found" }; + return pageSocialMetadata(appPath(app.slug), app.title, app.summary); +} + +export default async function AppDetailPage({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + const app = getAppBySlug(slug); + if (!app) notFound(); + + const groups = new Map(); + for (const source of app.sources) { + const group = source.group ?? appsCopy.sourcesTitle; + groups.set(group, [...(groups.get(group) ?? []), source]); + } + + return ( +
+ + {app.status && ( +
{appsCopy.statusLabels[app.status]}
+ )} +

{app.title}

+

{app.summary}

+
+ +
+

{appsCopy.sourcesTitle}

+

{app.sourceIntro}

+

{appsCopy.sourceDisclosure}

+

+ {appsCopy.reviewedLabel} ·{" "} + + {appsCopy.sourceEvidenceLabel} (opens in a new tab) + +

+ {Array.from(groups, ([group, sources]) => ( +
+

{group}

+ +
+ ))} +
+
+ ); +} diff --git a/src/app/apps/page.tsx b/src/app/apps/page.tsx new file mode 100644 index 0000000..0787ba0 --- /dev/null +++ b/src/app/apps/page.tsx @@ -0,0 +1,49 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { AppActions } from "@/components/AppActions"; +import { Badge } from "@/components/ui/badge"; +import { apps } from "@/content/apps"; +import { appsCopy } from "@/content/site-copy"; +import { appPath, APPS_PATH, pageSocialMetadata } from "@/lib/seo"; + +export const metadata: Metadata = pageSocialMetadata( + APPS_PATH, + appsCopy.title, + appsCopy.description, +); + +export default function AppsPage() { + return ( +
+

+ {appsCopy.title} +

+

+ {appsCopy.description} +

+
+ {apps.map((app) => ( +
+ {app.status && ( +
{appsCopy.statusLabels[app.status]}
+ )} +

+ + {app.title} + +

+

+ {app.summary} +

+

+ + {appsCopy.detailsLabel}: {app.title} + +

+ +
+ ))} +
+
+ ); +} diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 776f829..b7a6684 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -2,7 +2,10 @@ import type { MetadataRoute } from "next"; import { getActiveDatasets } from "@/lib/datasets"; import { getAllCollections } from "@/lib/collections"; import { allThemeSlugs, themePath } from "@/lib/landing"; +import { apps } from "@/content/apps"; import { + appPath, + APPS_PATH, collectionPath, COLLECTIONS_PATH, CONTRIBUTE_APP_PATH, @@ -15,6 +18,12 @@ export const dynamic = "force-static"; export default function sitemap(): MetadataRoute.Sitemap { return [ { url: SITE_URL, changeFrequency: "weekly", priority: 1 }, + { url: `${SITE_URL}${APPS_PATH}`, changeFrequency: "monthly", priority: 0.8 }, + ...apps.map((app) => ({ + url: `${SITE_URL}${appPath(app.slug)}`, + changeFrequency: "monthly" as const, + priority: 0.6, + })), { url: `${SITE_URL}${COLLECTIONS_PATH}`, changeFrequency: "weekly", priority: 0.8 }, { url: `${SITE_URL}${CONTRIBUTE_APP_PATH}`, changeFrequency: "monthly", priority: 0.4 }, ...allThemeSlugs().map(({ theme }) => ({ diff --git a/src/components/AppActions.tsx b/src/components/AppActions.tsx new file mode 100644 index 0000000..3190f45 --- /dev/null +++ b/src/components/AppActions.tsx @@ -0,0 +1,29 @@ +import { ExternalLink } from "lucide-react"; +import type { AppEntry } from "@/content/apps"; +import { appsCopy } from "@/content/site-copy"; + +export function AppActions({ app }: { app: AppEntry }) { + const actions = [ + ...(app.liveUrl ? [{ label: appsCopy.liveAppLabel, href: app.liveUrl }] : []), + ...(app.archiveUrl ? [{ label: appsCopy.archiveLabel, href: app.archiveUrl }] : []), + { label: appsCopy.sourceCodeLabel, href: app.sourceUrl }, + ]; + + return ( +
+ {actions.map(({ label, href }) => ( + + {app.title} {label} + + ))} +
+ ); +} diff --git a/src/components/SiteChrome.tsx b/src/components/SiteChrome.tsx index cb66f79..8a48c2c 100644 --- a/src/components/SiteChrome.tsx +++ b/src/components/SiteChrome.tsx @@ -4,7 +4,7 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import { useEffect, useRef } from "react"; import { siteCopy } from "@/content/site-copy"; -import { isCollectionsPath, isContributePath, isDatasetsPath } from "@/lib/nav"; +import { isAppsPath, isCollectionsPath, isContributePath, isDatasetsPath } from "@/lib/nav"; export function SiteHeader() { const pathname = usePathname(); @@ -22,6 +22,7 @@ export function SiteHeader() { diff --git a/src/content/apps.test.ts b/src/content/apps.test.ts new file mode 100644 index 0000000..9beb0b5 --- /dev/null +++ b/src/content/apps.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it } from "vitest"; +import { apps, getAppBySlug } from "./apps"; +import { getActiveDatasets } from "../lib/datasets"; + +const expectedApps = [ + { slug: "titanskies", title: "TitanSkies", status: undefined, liveUrl: "https://www.titanskies.com/", archiveUrl: undefined, sourceUrl: "https://github.com/hypertrial/titanskies" }, + { slug: "hyperoptions", title: "HyperOptions", status: undefined, liveUrl: undefined, archiveUrl: undefined, sourceUrl: "https://github.com/hypertrial/hyperoptions" }, + { slug: "travelcanary", title: "TravelCanary", status: "beta", liveUrl: "https://travelcanary.org/", archiveUrl: undefined, sourceUrl: "https://github.com/hypertrial/travelcanary" }, + { slug: "househunter", title: "HouseHunter", status: "alpha", liveUrl: undefined, archiveUrl: undefined, sourceUrl: "https://github.com/hypertrial/househunter" }, + { slug: "rockyroad", title: "RockyRoad", status: "alpha", liveUrl: undefined, archiveUrl: undefined, sourceUrl: "https://github.com/hypertrial/rockyroad" }, + { slug: "stackingsats", title: "StackingSats", status: "archived", liveUrl: undefined, archiveUrl: "https://stackingsats.org/", sourceUrl: "https://github.com/hypertrial/stacksats" }, +] as const; + +function expectHttps(raw: string) { + const url = new URL(raw); + expect(url.protocol, raw).toBe("https:"); + expect(url.hostname, raw).not.toBe(""); + expect(url.username, raw).toBe(""); + expect(url.password, raw).toBe(""); +} + +describe("reviewed app catalog", () => { + it("has exactly the six intended routes, availability states, and outbound destinations", () => { + expect(apps.map(({ slug, title, status, liveUrl, archiveUrl, sourceUrl }) => ({ + slug, title, status, liveUrl, archiveUrl, sourceUrl, + }))).toEqual(expectedApps); + expect(new Set(apps.map(({ slug }) => slug)).size).toBe(6); + expect(getAppBySlug("")).toBeUndefined(); + expect(getAppBySlug("travelcanary-extra")).toBeUndefined(); + expect(getAppBySlug("TravelCanary")).toBeUndefined(); + for (const app of apps) expect(getAppBySlug(app.slug)).toBe(app); + }); + + it("pins evidence and gives every source a usable disposition", () => { + const activeGuideIds = new Set(getActiveDatasets().map(({ id }) => id)); + for (const app of apps) { + expect(app.summary.trim(), app.slug).not.toBe(""); + expect(app.sourceIntro.trim(), app.slug).not.toBe(""); + expect(app.sources.length, app.slug).toBeGreaterThan(0); + expect(app.sourceRevision, app.slug).toMatch(/^[a-f0-9]{40}$/); + expect(app.reviewedAt, app.slug).toMatch(/^\d{4}-\d{2}-\d{2}$/); + const reviewDate = new Date(`${app.reviewedAt}T00:00:00Z`); + expect(reviewDate.toISOString().slice(0, 10), app.slug).toBe(app.reviewedAt); + expect(reviewDate.getTime(), app.slug).toBeLessThanOrEqual(Date.now()); + const today = new Date(); + const todayUtc = Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()); + expect(todayUtc - reviewDate.getTime(), `${app.slug}: source map is older than 90 days`).toBeLessThanOrEqual(90 * 24 * 60 * 60 * 1000); + for (const url of [app.sourceUrl, app.sourceEvidenceUrl, app.liveUrl, app.archiveUrl]) { + if (url) expectHttps(url); + } + expect(app.sourceEvidenceUrl, app.slug).toContain(app.sourceRevision); + + const names = new Set(); + for (const source of app.sources) { + expect(source.name.trim(), app.slug).not.toBe(""); + expect(names.has(source.name), `${app.slug}: ${source.name}`).toBe(false); + names.add(source.name); + expect(source.role.trim(), `${app.slug}: ${source.name}`).not.toBe(""); + expect(source.availability.trim(), `${app.slug}: ${source.name}`).not.toBe(""); + expectHttps(source.officialUrl); + if (source.evidenceUrl) expectHttps(source.evidenceUrl); + for (const { href } of source.additionalUrls ?? []) expectHttps(href); + expect(Boolean(source.guideId), `${app.slug}: ${source.name}`).not.toBe(Boolean(source.noGuideReason)); + if (source.guideId) expect(activeGuideIds.has(source.guideId), `${app.slug}: ${source.name}`).toBe(true); + if (source.noGuideReason) expect(source.noGuideReason.trim().length, `${app.slug}: ${source.name}`).toBeGreaterThan(10); + } + } + }); + + it("links confirmed feed families only to their matching guides", () => { + for (const [slug, sourceName, guideId] of [ + ["titanskies", /AirNow/i, "airnow-air-quality"], + ["travelcanary", /USGS earthquake/i, "usgs-earthquakes"], + ["travelcanary", /GDACS/i, "gdacs-disaster-alerts"], + ["househunter", /TIGER\/Line/i, "census-tiger-line"], + ["hyperoptions", /SEC EDGAR/i, "sec-edgar-apis"], + ] as const) { + const source = getAppBySlug(slug)?.sources.find(({ name }) => sourceName.test(name)); + expect(source, `${slug}: ${sourceName}`).toBeDefined(); + expect(source?.guideId, `${slug}: ${sourceName}`).toBe(guideId); + } + for (const [slug, name] of [ + ["househunter", "FEMA National Risk Index"], + ["rockyroad", "OpenStreetMap regional extracts via Geofabrik"], + ] as const) { + const source = getAppBySlug(slug)?.sources.find((entry) => entry.name === name); + expect(source, `${slug}: ${name}`).toBeDefined(); + expect(source?.guideId, `${slug}: ${name}`).toBeUndefined(); + expect(source?.noGuideReason, `${slug}: ${name}`).toMatch(/different|does not cover/i); + } + expect(getAppBySlug("stackingsats")?.sourceIntro).toMatch(/historical|archiv/i); + expect(getAppBySlug("travelcanary")?.sources.length).toBeGreaterThan(20); + }); +}); diff --git a/src/content/apps.ts b/src/content/apps.ts new file mode 100644 index 0000000..cc8c03f --- /dev/null +++ b/src/content/apps.ts @@ -0,0 +1,45 @@ +import { otherApps } from "./apps/other"; +import { travelCanary } from "./apps/travelcanary"; + +export type AppSource = { + name: string; + group?: string; + role: string; + availability: string; + officialUrl: string; + additionalUrls?: readonly { label: string; href: string }[]; + evidenceUrl?: string; + coverage?: string; + details?: string; +} & ( + | { guideId: string; noGuideReason?: never } + | { guideId?: never; noGuideReason: string } +); + +export type AppEntry = { + slug: string; + title: string; + summary: string; + status?: "alpha" | "beta" | "archived"; + liveUrl?: string; + archiveUrl?: string; + sourceUrl: string; + sourceRevision: string; + sourceEvidenceUrl: string; + reviewedAt: string; + sourceIntro: string; + sources: readonly AppSource[]; +}; + +export const apps: readonly AppEntry[] = [ + otherApps[0], + otherApps[1], + travelCanary, + otherApps[2], + otherApps[3], + otherApps[4], +]; + +export function getAppBySlug(slug: string): AppEntry | undefined { + return apps.find((app) => app.slug === slug); +} diff --git a/src/content/apps/other.ts b/src/content/apps/other.ts new file mode 100644 index 0000000..d97be87 --- /dev/null +++ b/src/content/apps/other.ts @@ -0,0 +1,462 @@ +// Reviewed against the product repositories at the pinned revisions below. +// Keep source availability distinct from the mere presence of an integration. +export const otherApps = [ + { + slug: "titanskies", + title: "TitanSkies", + summary: "Explore wildfire smoke forecasts, air-quality readings, and reported fire context across North America.", + liveUrl: "https://www.titanskies.com/", + sourceUrl: "https://github.com/hypertrial/titanskies", + sourceRevision: "3cf6db94229dc8eb195f4caf662da01eb0e83691", + sourceEvidenceUrl: "https://github.com/hypertrial/titanskies/blob/3cf6db94229dc8eb195f4caf662da01eb0e83691/shared/data-sources.json", + reviewedAt: "2026-09-28", + sourceIntro: "TitanSkies combines two smoke models with preliminary monitoring data and agency-reported wildfire context. Individual inputs can be stale or unavailable; forecasts are not observations or emergency alerts.", + sources: [ + { + name: "ECCC FireWork", + group: "Forecasts", + role: "Base smoke-concentration forecast for the integrated map.", + availability: "Configured public WCS forecast; a failed or stale run can retain a dated prior forecast.", + officialUrl: "https://www.weather.gc.ca/firework/index_e.html", + coverage: "Canada and adjoining areas covered by the FireWork model.", + noGuideReason: "A FireWork-specific guide has not passed the Data catalog review.", + }, + { + name: "NOAA HRRR-Smoke", + group: "Forecasts", + role: "U.S. smoke forecast enhancement where it exceeds the FireWork base.", + availability: "Configured public GRIB forecast; freshness and fallback are checked per run.", + officialUrl: "https://rapidrefresh.noaa.gov/hrrr/HRRRsmoke/", + coverage: "United States model domain.", + noGuideReason: "A HRRR-Smoke-specific guide has not passed the Data catalog review.", + }, + { + name: "EPA AirNow", + group: "Observations", + role: "Provider-reported PM2.5 AQI at participating monitors; TitanSkies does not recalculate these AQI values.", + availability: "Optional: requires AIRNOW_API_KEY. Missing key or provider failure makes this monitor set unavailable or dated.", + officialUrl: "https://docs.airnowapi.org/webservices", + coverage: "Participating U.S. monitoring sites.", + guideId: "airnow-air-quality", + }, + { + name: "B.C. ENV air quality", + group: "Observations", + role: "Hourly PM2.5 concentrations converted by TitanSkies to U.S. EPA NowCast AQI.", + availability: "Configured public observation feed; stations and timestamps may be incomplete or stale.", + officialUrl: "https://www2.gov.bc.ca/gov/content/environment/air-land-water/air/air-quality/current-air-quality-data", + coverage: "British Columbia stations.", + noGuideReason: "A guide for this B.C. hourly feed has not passed catalog review.", + }, + { + name: "INECC SINAICA", + group: "Observations", + role: "Hourly PM2.5 concentrations quality-screened and converted to U.S. EPA NowCast AQI.", + availability: "Gated by SINAICA_ENABLED; disabled sources and failed reads are reported as unavailable or dated.", + officialUrl: "https://sinaica.inecc.gob.mx/", + coverage: "Participating Mexico monitoring stations.", + noGuideReason: "A SINAICA-specific guide has not passed catalog review.", + }, + { + name: "ECCC Air Quality Health Index", + group: "Observations", + role: "Canadian AQHI observations, kept on the AQHI scale rather than converted to AQI.", + availability: "Configured public feed; observations can be missing or stale.", + officialUrl: "https://weather.gc.ca/airquality/pages/index_e.html", + coverage: "Participating Canadian stations.", + noGuideReason: "An AQHI observation guide has not passed catalog review.", + }, + { + name: "NIFC WFIGS", + group: "Wildfire context", + role: "Agency-reported U.S. wildfire incident points and perimeters; they do not identify a smoke plume's origin.", + availability: "Configured public service with per-run freshness checks and dated fallback.", + officialUrl: "https://data-nifc.opendata.arcgis.com/", + coverage: "United States incidents and perimeters.", + noGuideReason: "A WFIGS incident-layer guide has not passed catalog review.", + }, + { + name: "NRCan CWFIS", + group: "Wildfire context", + role: "Agency-reported Canadian wildfire incident points and perimeters.", + availability: "Configured public service with per-run freshness checks and dated fallback.", + officialUrl: "https://cwfis.cfs.nrcan.gc.ca/", + coverage: "Canada incidents and perimeters.", + noGuideReason: "A CWFIS incident-layer guide has not passed catalog review.", + }, + { + name: "Natural Earth oceans and populated places", + group: "Pinned geography", + role: "Ocean mask for smoke coverage and city candidates for ranked smoke and AQI results and search.", + availability: "Pinned 50m ocean and 10m populated-place source artifacts used during the geography build, not a live feed.", + officialUrl: "https://www.naturalearthdata.com/", + evidenceUrl: "https://github.com/hypertrial/titanskies/blob/3cf6db94229dc8eb195f4caf662da01eb0e83691/scripts/generate_geo.py", + coverage: "North America within the generated map and city catalog.", + noGuideReason: "The existing Natural Earth guide covers 110m country boundaries, not these 50m ocean and 10m populated-place artifacts.", + }, + ], + }, + { + slug: "hyperoptions", + title: "HyperOptions", + summary: "Explore Nasdaq-listed covered calls and cash-secured puts with dated market context and stock forecasts.", + sourceUrl: "https://github.com/hypertrial/hyperoptions", + sourceRevision: "e23909d37cdf65f40967bc5c4194a8dcda967602", + sourceEvidenceUrl: "https://github.com/hypertrial/hyperoptions/blob/e23909d37cdf65f40967bc5c4194a8dcda967602/README.md", + reviewedAt: "2026-09-28", + sourceIntro: "HyperOptions is a local workstation. Its chain and watch results depend on public market endpoints; some research methods remain unavailable until input rights or evidence qualify. It is not a brokerage or trading recommendation.", + sources: [ + { + name: "Nasdaq public market endpoints", + group: "Market data", + role: "Listed-stock universe, stock quote, option chains, and completed daily price lows; also cross-checks expiry closes.", + availability: "Enabled, unofficial public JSON access for personal local use; chain and new watches fail closed if the ticker universe cannot load.", + officialUrl: "https://www.nasdaq.com/market-activity/stocks", + evidenceUrl: "https://github.com/hypertrial/hyperoptions/blob/e23909d37cdf65f40967bc5c4194a8dcda967602/backend/src/options_api/nasdaq.py", + noGuideReason: "No Nasdaq option-chain guide has passed the Data catalog's access and reuse review.", + }, + { + name: "Yahoo Finance", + group: "Market data", + role: "Completed stock prices for physical forecasts and provisional expiry results, dividend status that gates market-implied odds, and option-chain replacement when Nasdaq coverage is truncated.", + availability: "Enabled through an unofficial client, with verified local caches. Training on Yahoo-derived histories is gated while rights remain unverified.", + officialUrl: "https://finance.yahoo.com/", + evidenceUrl: "https://github.com/hypertrial/hyperoptions/blob/e23909d37cdf65f40967bc5c4194a8dcda967602/docs/source-rights.md", + noGuideReason: "Automated history and option-quote reuse rights have not qualified for a Data guide.", + }, + { + name: "U.S. Treasury interest rates", + group: "Market data", + role: "Dated yield curve used as the risk-free rate for option Greeks and market-implied odds.", + availability: "Configured public XML source; unavailable dates withhold dependent calculations.", + officialUrl: "https://home.treasury.gov/resource-center/data-chart-center/interest-rates/pages/xml", + evidenceUrl: "https://github.com/hypertrial/hyperoptions/blob/e23909d37cdf65f40967bc5c4194a8dcda967602/backend/src/options_api/market_sources.py", + noGuideReason: "The existing Treasury auctions guide covers a different data product; yield-curve guide review is pending.", + }, + { + name: "SEC EDGAR submissions", + group: "Research inputs", + role: "Explicit future earnings-date announcements from selected 8-K exhibits for the gated earnings-jump research method.", + availability: "Deliberate, contact-identified capture only; it does not run at startup and a filing date alone never enables the model.", + officialUrl: "https://www.sec.gov/search-filings/edgar-application-programming-interfaces", + evidenceUrl: "https://github.com/hypertrial/hyperoptions/blob/e23909d37cdf65f40967bc5c4194a8dcda967602/docs/source-rights.md", + guideId: "sec-edgar-apis", + }, + ], + }, + { + slug: "househunter", + title: "HouseHunter", + summary: "Compare U.S. residential hazards and county context with pinned public data and optional private home-cost imports.", + status: "alpha", + sourceUrl: "https://github.com/hypertrial/househunter", + sourceRevision: "eea3dfffb5bd728afff4e63edda27959e7d86f97", + sourceEvidenceUrl: "https://github.com/hypertrial/househunter/blob/eea3dfffb5bd728afff4e63edda27959e7d86f97/DATA_SOURCES.md", + reviewedAt: "2026-09-28", + sourceIntro: "HouseHunter pins source vintages for its local hazard map and county fit tools. Normal runs use verified caches and packaged ranking references; maintainer-only inputs are not live feeds. Home-market history is private, imported only by the user, and never published here.", + sources: [ + { + name: "FEMA National Risk Index", + group: "Core hazard map", + role: "Separate tract and county building-loss inputs for 17 hazards; HouseHunter derives residential hazard percentiles at each grain.", + availability: "Pinned v1.20 (December 2025) ArcGIS tract and county layers. Runtime downloads verify revisions and cache them locally.", + officialUrl: "https://hazards.fema.gov/nri/data-resources", + additionalUrls: [ + { label: "Tract layer", href: "https://www.arcgis.com/home/item.html?id=9da4eeb936544335a6db0cd7a8448a51&sublayer=0" }, + { label: "County layer", href: "https://www.arcgis.com/home/item.html?id=39485e8035d446a5bff03259508ae355&sublayer=0" }, + ], + coverage: "85,154 tracts and 3,232 counties/county equivalents in the pinned source layers.", + noGuideReason: "FEMA National Flood Hazard Layer is a different dataset; a National Risk Index guide has not passed review.", + }, + { + name: "County Health Rankings & Roadmaps 2025", + group: "Core hazard map", + role: "Official county Community Conditions Health Group plus a separate NPPES-derived mental-health supplement used in County Fit.", + availability: "Pinned 2025 ArcGIS county layer and 2025 supplemental file; the supplement is acquired by maintainers, not at app startup.", + officialUrl: "https://www.countyhealthrankings.org/health-data/methodology-and-sources/data-documentation", + additionalUrls: [ + { label: "Community Conditions county layer", href: "https://p3eplmys2rvchkjx.svcs.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/County%20Health%20Rankings%202025/FeatureServer/2" }, + { label: "Mental-health supplement", href: "https://www.countyhealthrankings.org/sites/default/files/media/document/analytic_supplement_20260325%5B1%5D.csv" }, + ], + noGuideReason: "The NPPES registry guide does not cover these CHR&R published products; guide review is pending.", + }, + { + name: "BEA Regional Price Parities", + group: "County fit and context", + role: "2024 MSA all-items RPP for the map and county fit; official state RPP supplements nonmetro ranking assignments.", + availability: "Pinned MARPP and SARPP 2024 archives. BEA map context is optional and can be null when its verified download fails.", + officialUrl: "https://www.bea.gov/data/prices-inflation/regional-price-parities-state-and-metro-area", + additionalUrls: [ + { label: "MSA archive", href: "https://apps.bea.gov/regional/zip/MARPP.zip" }, + { label: "State archive", href: "https://apps.bea.gov/regional/zip/SARPP.zip" }, + ], + noGuideReason: "The existing BEA GDP and income guide does not cover Regional Price Parities.", + }, + { + name: "Realtor.com Research Data", + group: "Private local input", + role: "County home-cost month and trailing-12-month history for Home Costs, Affordability, and Custom Fit.", + availability: "Private, explicit local import of a checksum-approved file for acknowledged personal use. No rows are packaged, fetched automatically, or exposed by this site.", + officialUrl: "https://www.realtor.com/research/data/", + noGuideReason: "Reuse is restricted and the data is available only through the user's private local import.", + }, + { + name: "Census Population Estimates Program", + group: "Pinned county ranking", + role: "2025 county population for ranking eligibility and context, including the fixed 25,000-person floor.", + availability: "Pinned 2025 county totals, acquired by maintainers and packaged only as derived county reference values.", + officialUrl: "https://www.census.gov/programs-surveys/popest.html", + additionalUrls: [{ label: "Pinned county file", href: "https://www2.census.gov/programs-surveys/popest/datasets/2020-2025/counties/totals/co-est2025-alldata.csv" }], + noGuideReason: "A Census PEP county-totals guide has not passed catalog review.", + }, + { + name: "FBI Crime Data Explorer", + group: "Pinned county ranking", + role: "2023–2025 attributed agency crime and reporting-population coverage for Safety Factors.", + availability: "Pinned agency response manifest and manually staged reference archives; suppression and less than 90% coverage remain null.", + officialUrl: "https://cde.ucr.cjis.gov/LATEST/webapp/", + evidenceUrl: "https://github.com/hypertrial/househunter/blob/eea3dfffb5bd728afff4e63edda27959e7d86f97/config/ranking/source-lock-v2.json", + noGuideReason: "The existing FBI guide starts with state estimates, not this pinned agency response and archive contract.", + }, + { + name: "EPA Safe Drinking Water Information System", + group: "Pinned county ranking", + role: "Public-water-system inventory and violation records contributing to Safety Factors.", + availability: "Pinned 2026Q2 bulk submission staged by maintainers; excludes private wells and is not real-time water-quality measurement.", + officialUrl: "https://echo.epa.gov/tools/data-downloads/sdwa-download-summary", + noGuideReason: "The existing ECHO web-service guide is not the pinned SDWIS bulk archive used here.", + }, + { + name: "EPA Community Water System Service Areas", + group: "Pinned county ranking", + role: "Modeled and supplied public-water boundaries allocated to 2020 Census block population for county coverage context.", + availability: "Pinned version 2.1 GeoPackage and block table, acquired by maintainers; insufficient allocatable coverage remains null.", + officialUrl: "https://www.epa.gov/ground-water-and-drinking-water/public-water-system-service-areas", + noGuideReason: "A guide for the service-area model and allocation table has not passed review.", + }, + { + name: "HRSA Area Health Resources Files", + group: "Pinned county ranking", + role: "2024–2025 county primary-care and dental provider supply for Health utility.", + availability: "Pinned AHRF archive acquired by maintainers; provider counts are availability proxies.", + officialUrl: "https://data.hrsa.gov/data/download", + noGuideReason: "An AHRF county guide has not passed catalog review.", + }, + { + name: "Census ACS 2024 Five-Year Estimates", + group: "Pinned county ranking", + role: "Housing age/value and property tax, commute context, and county-to-CBSA geography used by the ranking and housing-stock references.", + availability: "Pinned table-based summary files B25034, B25035, B25103, B25077, and B08303; maintainer-generated housing-stock assets are bundled, not fetched at runtime.", + officialUrl: "https://www.census.gov/programs-surveys/acs/data/summary-file.html", + coverage: "Tract and county housing stock across the 50 states, DC, and Puerto Rico; separate county ranking inputs.", + noGuideReason: "The existing ACS guide starts from the Data API, not these pinned table-based Summary File artifacts.", + }, + { + name: "BLS Quarterly Census of Employment and Wages", + group: "Pinned county ranking", + role: "County employment and wage context for Opportunity utility.", + availability: "Pinned final 2024 and 2025 county high-level archives acquired by maintainers.", + officialUrl: "https://www.bls.gov/cew/", + noGuideReason: "The BLS Public Data API guide covers a different access product; a QCEW archive guide is pending.", + }, + { + name: "FCC Broadband Data Collection", + group: "Pinned county ranking", + role: "December 2025 terrestrial fixed 100/20 served share of broadband-serviceable locations for Opportunity utility.", + availability: "Pinned county summary archive staged by maintainers; Location Fabric is denied and this is not population coverage.", + officialUrl: "https://www.fcc.gov/BroadbandData", + noGuideReason: "The existing National Broadband Map guide uses the API, not the manually staged county summary archive.", + }, + { + name: "NOAA U.S. Climate Normals", + group: "Pinned county ranking", + role: "1991–2020 annual and monthly in-county station climate values for optional filters.", + availability: "Pinned v1.0.1 station archives acquired by maintainers; climate is null without a fully qualifying station.", + officialUrl: "https://www.ncei.noaa.gov/products/land-based-station/us-climate-normals", + noGuideReason: "A Climate Normals station archive guide has not passed catalog review.", + }, + { + name: "Census TIGER/Line", + group: "Geography and mountain inputs", + role: "2025 county boundaries for ranking and 2020 census blocks with population/internal points for Mountain Magnitude.", + availability: "Pinned 2025 national county ZIP plus 51 state/DC 2020 block extracts; Mountain build is maintainer-only.", + officialUrl: "https://www.census.gov/geographies/mapping-files/time-series/geo/tiger-line-file.html", + coverage: "50 states and DC; 2020 block and 2025 county vintages are kept distinct.", + guideId: "census-tiger-line", + }, + { + name: "USGS 3D Elevation Program", + group: "Geography and mountain inputs", + role: "Elevation rasters for Mountain Magnitude terrain relief, slope, and ruggedness.", + availability: "Pinned 1,645 reviewed 1-arc-second tiles; maintainer-only acquisition and no normal startup fetch.", + officialUrl: "https://www.usgs.gov/3d-elevation-program", + evidenceUrl: "https://github.com/hypertrial/househunter/blob/eea3dfffb5bd728afff4e63edda27959e7d86f97/config/mountain/source-lock-v2.json", + noGuideReason: "A 3DEP elevation-tile guide has not passed catalog review.", + }, + { + name: "USGS Protected Areas Database", + group: "Geography and mountain inputs", + role: "PAD-US 4.1 public-access polygons for Mountain Magnitude open-land context.", + availability: "Pinned anonymous MapServer object inventory; maintainer-only capture.", + officialUrl: "https://www.usgs.gov/programs/gap-analysis-project/science/pad-us-data-download", + evidenceUrl: "https://github.com/hypertrial/househunter/blob/eea3dfffb5bd728afff4e63edda27959e7d86f97/config/mountain/source-lock-v2.json", + noGuideReason: "A PAD-US 4.1 layer guide has not passed catalog review.", + }, + { + name: "USGS National Transportation Database trails", + group: "Geography and mountain inputs", + role: "Mapped hiking trail segments for Mountain Magnitude access distance and density.", + availability: "Pinned 51 state/DC GPKG extracts dated 2026-02-12; maintainer-only build input.", + officialUrl: "https://www.usgs.gov/national-digital-trails/how-access-or-view-usgs-trails-dataset", + evidenceUrl: "https://github.com/hypertrial/househunter/blob/eea3dfffb5bd728afff4e63edda27959e7d86f97/config/mountain/source-lock-v2.json", + noGuideReason: "A National Transportation Database trail-extract guide has not passed review.", + }, + { + name: "Appalachian Regional Commission county list", + group: "Pinned county ranking", + role: "Appalachia region membership for County Fit filters.", + availability: "Reviewed 2026 county list packaged as a fixed FIPS reference.", + officialUrl: "https://www.arc.gov/appalachian-counties-served-by-arc/", + evidenceUrl: "https://github.com/hypertrial/househunter/blob/eea3dfffb5bd728afff4e63edda27959e7d86f97/config/ranking/appalachia-counties.json", + noGuideReason: "A guide for the ARC county list has not passed catalog review.", + }, + { + name: "State homeschool law references", + group: "Pinned county ranking", + role: "Official state statute references inform HouseHunter's approximate, project-authored Homeschool Policy Fit rubric.", + availability: "Pinned state-level rubric reviewed 2026-09-16; it is a preference proxy, not legal advice or current compliance guidance.", + officialUrl: "https://www.usa.gov/state-governments", + additionalUrls: [{ + label: "51 Pinned Official Statute Citations", + href: "https://github.com/hypertrial/househunter/blob/eea3dfffb5bd728afff4e63edda27959e7d86f97/config/ranking/homeschool-policy-v1.json", + }], + coverage: "50 states and DC; the pinned rubric contains each jurisdiction's official statute URL.", + noGuideReason: "A project-authored preference rubric is outside the external dataset catalog.", + }, + { + name: "Census Geocoder", + group: "On-demand address lookup", + role: "Matches a user-entered address or fallback coordinates to its 2020 Census tract.", + availability: "Called only on explicit lookup; a provider outage is not treated as an empty match.", + officialUrl: "https://geocoding.geo.census.gov/geocoder/", + noGuideReason: "A Census Geocoder lookup guide has not passed catalog review.", + }, + { + name: "OpenStreetMap Nominatim", + group: "On-demand address lookup", + role: "Fallback street search only when Census returns a valid empty address-match list; Census still supplies the tract ID.", + availability: "Optional HTTPS endpoint, disableable by operator; approximate road matches require user confirmation.", + officialUrl: "https://nominatim.openstreetmap.org/", + noGuideReason: "Nominatim address search is a different service from the existing OSM Overpass guide.", + }, + ], + }, + { + slug: "rockyroad", + title: "RockyRoad", + summary: "Plan Canada–U.S. road trips with hosted search and routing or an explicitly built local OpenStreetMap dataset.", + status: "alpha", + sourceUrl: "https://github.com/hypertrial/rockyroad", + sourceRevision: "5acf684a8de1ea0cdb85506ecda72dbe73e150b3", + sourceEvidenceUrl: "https://github.com/hypertrial/rockyroad/blob/5acf684a8de1ea0cdb85506ecda72dbe73e150b3/docs/data-build.md", + reviewedAt: "2026-09-28", + sourceIntro: "RockyRoad defaults to hosted routing and search. An explicit local mode replaces them with artifacts built from allow-listed OSM extracts; it does not silently fail over between modes. Map presentation tiles are separate from the product-data sources below.", + sources: [ + { + name: "OpenRouteService and VROOM", + group: "Hosted mode", + role: "Driving directions and ordered trip stops.", + availability: "Default hosted routing requires ROCKYROAD_ORS_API_KEY; quota, outage, or missing key can block route building.", + officialUrl: "https://openrouteservice.org/", + additionalUrls: [{ label: "Hosted optimization API", href: "https://api.heigit.org/vroom/v0/optimization" }], + coverage: "Canada and United States within the hosted provider's route limits.", + noGuideReason: "A provider-specific directions and optimization guide has not passed catalog review.", + }, + { + name: "Photon", + group: "Hosted mode", + role: "Place search and stop selection, with cached and deduplicated results.", + availability: "Default hosted public demo service; usage limits and outages can interrupt search.", + officialUrl: "https://photon.komoot.io/", + noGuideReason: "A Photon geocoding guide has not passed catalog review.", + }, + { + name: "OpenStreetMap regional extracts via Geofabrik", + group: "Local mode", + role: "Local places and road graph built from OSM PBF files for search and routing.", + availability: "Optional explicit local build; sample profile pins Prince Edward Island, full profile allows Canada and U.S. extracts. No runtime hosted-geocoder or routing call in this mode.", + officialUrl: "https://download.geofabrik.de/north-america.html", + additionalUrls: [{ label: "OSM data and license", href: "https://www.openstreetmap.org/copyright" }], + coverage: "PEI sample or Canada plus United States, depending on the selected extract profile.", + evidenceUrl: "https://github.com/hypertrial/rockyroad/blob/5acf684a8de1ea0cdb85506ecda72dbe73e150b3/config/regions.yaml", + noGuideReason: "The existing OSM Overpass guide does not cover Geofabrik PBF extracts.", + }, + ], + }, + { + slug: "stackingsats", + title: "StackingSats", + summary: "Archived Bitcoin accumulation research site and Python library for testing fixed-budget allocation strategies.", + status: "archived", + archiveUrl: "https://stackingsats.org/", + sourceUrl: "https://github.com/hypertrial/stacksats", + sourceRevision: "9ba73643c4478ddd92650557a90e91e64674f587", + sourceEvidenceUrl: "https://github.com/hypertrial/stacksats/blob/9ba73643c4478ddd92650557a90e91e64674f587/docs/data-source.md", + reviewedAt: "2026-09-28", + sourceIntro: "The archived website is informational. The historical StackSats library uses a manually fetched, hash-verified BRK parquet and can optionally call market-price helpers. Backtests never imply that the archived site serves a live feed.", + sources: [ + { + name: "Bitcoin Research Kit merged metrics", + group: "Historical library data", + role: "Canonical long-format Bitcoin metrics projected into a local wide parquet for strategy research and backtests.", + availability: "Pinned 2026-03-15 Google Drive artifact; explicit fetch verifies 1.16 GB size and SHA-256. Runtime reads local parquet and never auto-downloads.", + officialUrl: "https://github.com/bitcoinresearchkit/brk", + additionalUrls: [{ label: "Pinned parquet artifact", href: "https://drive.google.com/file/d/1jKRRU7l9kOMdGI_hIJGg02X3jWTMPJsw/view?usp=sharing" }], + evidenceUrl: "https://github.com/hypertrial/stacksats/blob/9ba73643c4478ddd92650557a90e91e64674f587/stacksats/assets/brk_data_manifest.json", + coverage: "Snapshot documents 2009-01-03 through 2026-03-13 daily metrics.", + noGuideReason: "A BRK merged-metrics guide has not passed the Data catalog's access and reuse review.", + }, + { + name: "CoinGecko", + group: "Optional library price helpers", + role: "First-choice current BTC/USD quote for export helpers and historical-date lookup helper.", + availability: "Optional network extra and explicit helper calls only; no archived-website feed.", + officialUrl: "https://www.coingecko.com/en/api", + evidenceUrl: "https://github.com/hypertrial/stacksats/blob/9ba73643c4478ddd92650557a90e91e64674f587/stacksats/data/btc_price_fetcher.py", + noGuideReason: "A CoinGecko price API guide has not passed catalog review.", + }, + { + name: "Coinbase", + group: "Optional library price helpers", + role: "Fallback current BTC/USD spot quote for export helpers.", + availability: "Optional network extra and explicit helper calls only; no archived-website feed.", + officialUrl: "https://api.coinbase.com/v2/prices/BTC-USD/spot", + noGuideReason: "A Coinbase spot-price guide has not passed catalog review.", + }, + { + name: "Bitstamp", + group: "Optional library price helpers", + role: "Fallback current BTC/USD ticker price for export helpers.", + availability: "Optional network extra and explicit helper calls only; no archived-website feed.", + officialUrl: "https://www.bitstamp.net/api/", + noGuideReason: "A Bitstamp ticker guide has not passed catalog review.", + }, + { + name: "Kraken", + group: "Optional library price helpers", + role: "Fallback current BTC/USD ticker price for export helpers.", + availability: "Optional network extra and explicit helper calls only; no archived-website feed.", + officialUrl: "https://docs.kraken.com/api/docs/rest-api/get-ticker-information/", + noGuideReason: "A Kraken ticker guide has not passed catalog review.", + }, + { + name: "Binance", + group: "Optional library price helpers", + role: "Fallback current BTC/USDT price and historical kline lookup helper.", + availability: "Optional network extra and explicit helper calls only; BTC/USDT is a proxy for USD, not the same quote.", + officialUrl: "https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints", + noGuideReason: "A Binance market-data guide has not passed catalog review.", + }, + ], + }, +] as const; diff --git a/src/content/apps/travelcanary.ts b/src/content/apps/travelcanary.ts new file mode 100644 index 0000000..7084191 --- /dev/null +++ b/src/content/apps/travelcanary.ts @@ -0,0 +1,846 @@ +// Reviewed against TravelCanary's generated source inventory and its authored +// source registries. This is a static editorial snapshot, not runtime health. +export const travelCanary = { + slug: "travelcanary", + title: "TravelCanary", + summary: "A destination-level view of current hazards and local conditions across 45 European countries.", + status: "beta", + liveUrl: "https://travelcanary.org/", + sourceUrl: "https://github.com/hypertrial/travelcanary", + sourceRevision: "1ff2fdfa06b38880175a03aed10cfe60352bd0f6", + sourceEvidenceUrl: "https://github.com/hypertrial/travelcanary/blob/1ff2fdfa06b38880175a03aed10cfe60352bd0f6/data/source-inventory.json", + reviewedAt: "2026-09-28", + sourceIntro: "These are the source families and pinned inputs in the reviewed TravelCanary revision, including optional and unavailable connectors. A listing does not establish that a feed is running, healthy, or applicable to a destination. Warnings can contribute only within reviewed source geometry; forecasts, observations, satellite detections, news, and travel advice are context or discovery.", + sources: [ + { + group: "Hazard and discovery feeds", + name: "MeteoAlarm / EUMETNET warnings", + role: "Official severe-weather and related hazard warnings; keyless Atom primary feed and optional authenticated EDR recovery.", + availability: "Configured. Country and destination mappings vary; EDR recovery requires a token and does not add coverage independently.", + coverage: "European member feeds; the country-by-country active and gated mappings are listed under National warnings.", + officialUrl: "https://meteoalarm.org/", + noGuideReason: "No matching MeteoAlarm warning-feed guide is yet in the Data catalog.", + }, + { + group: "Hazard and discovery feeds", + name: "USGS earthquake feeds", + role: "Primary earthquake event evidence, including the product's published application feeds and ShakeMap context.", + availability: "Configured; individual destinations receive evidence only when the event and coverage checks apply.", + officialUrl: "https://earthquake.usgs.gov/", + guideId: "usgs-earthquakes", + }, + { + group: "Hazard and discovery feeds", + name: "Copernicus EFFIS", + role: "Fire-danger forecasts and active-fire context; danger forecast does not confirm a fire.", + availability: "Configured; active-fire detections and perimeters are complementary and do not establish official wildfire-warning coverage.", + officialUrl: "https://forest-fire.emergency.copernicus.eu/", + noGuideReason: "No matching EFFIS fire-danger or active-fire guide is yet in the Data catalog.", + }, + { + group: "Hazard and discovery feeds", + name: "NASA FIRMS thermal detections", + role: "Satellite hotspot context alongside EFFIS active-fire data; detections are not confirmed wildfire perimeters.", + availability: "Optional; the FIRMS path needs an operator MAP_KEY and cannot establish warning coverage.", + officialUrl: "https://firms.modaps.eosdis.nasa.gov/", + guideId: "nasa-firms", + }, + { + group: "Hazard and discovery feeds", + name: "Copernicus EMS Rapid Mapping", + role: "Complementary maps for wildfire, flood, industrial, and other emergency events.", + availability: "Configured as non-blocking context; a Rapid Mapping activation is not current warning coverage.", + officialUrl: "https://mapping.emergency.copernicus.eu/", + noGuideReason: "No matching Rapid Mapping activation guide is yet in the Data catalog.", + }, + { + group: "Hazard and discovery feeds", + name: "GDACS disaster alerts", + role: "Hourly disaster-event discovery used to route checks to independent official sources.", + availability: "Configured for discovery only; GDACS never changes risk or satisfies warning coverage.", + officialUrl: "https://www.gdacs.org/", + guideId: "gdacs-disaster-alerts", + }, + { + group: "Hazard and discovery feeds", + name: "Copernicus Global Flood Monitoring", + role: "Satellite flood corroboration.", + availability: "Environment-gated and non-blocking; no official flood-warning coverage credit.", + officialUrl: "https://global-flood.emergency.copernicus.eu/", + noGuideReason: "No matching Global Flood Monitoring guide is yet in the Data catalog.", + }, + { + group: "Hazard and discovery feeds", + name: "EMSC Seismic Portal", + role: "Preliminary earthquake fallback when primary USGS evidence is unavailable.", + availability: "Configured as fallback; unmatched reports cannot replace ShakeMap intensity or establish monitoring coverage.", + officialUrl: "https://www.seismicportal.eu/", + noGuideReason: "The USGS guide describes a different earthquake feed; an EMSC guide needs separate review.", + }, + { + group: "Hazard and discovery feeds", + name: "SLF avalanche bulletins", + role: "Official avalanche warnings for mapped Swiss bulletin regions.", + availability: "Configured; limited to destinations intersecting reviewed bulletin geometry.", + officialUrl: "https://www.slf.ch/en/avalanche-bulletin-and-snow-situation/", + noGuideReason: "No matching SLF bulletin guide is yet in the Data catalog.", + }, + { + group: "Hazard and discovery feeds", + name: "Avalanche.report / EAWS bulletins", + role: "Official avalanche warnings for licensed, reviewed Alpine regions.", + availability: "Configured only for mapped feed regions; additional region intersections remain gated pending bulletin verification.", + officialUrl: "https://avalanche.report/", + noGuideReason: "No matching Avalanche.report bulletin guide is yet in the Data catalog.", + }, + { + group: "Hazard and discovery feeds", + name: "European Environment Agency air quality", + role: "Observed station pollutants and European AQI context.", + availability: "Configured with partial monitoring only at mapped operational stations; modeled or gap-filled values cannot establish an all-clear.", + officialUrl: "https://airindex.eea.europa.eu/", + noGuideReason: "No matching EEA station or AQI artifact guide is yet in the Data catalog.", + }, + { + group: "Hazard and discovery feeds", + name: "GDELT news discovery", + role: "Corroborated civil-unrest and security news context.", + availability: "Gated; requires three independently owned reviewed publishers and never establishes monitoring coverage.", + officialUrl: "https://www.gdeltproject.org/", + noGuideReason: "No matching GDELT discovery-feed guide is yet in the Data catalog.", + }, + { + group: "Hazard and discovery feeds", + name: "Vigicrues flood warnings", + role: "Official French river-section flood warnings.", + availability: "Configured; only destinations mapped to official river sections qualify.", + officialUrl: "https://www.vigicrues.gouv.fr/", + noGuideReason: "No matching Vigicrues warning guide is yet in the Data catalog.", + }, + { + group: "Hazard and discovery feeds", + name: "Swiss FOEN flood-warning map", + role: "Official Swiss flood-warning map sampled at destination geometry.", + availability: "Configured for the mapped Swiss footprint; a warning-map pixel is not a local water-level measurement.", + officialUrl: "https://www.hydrodaten.admin.ch/", + noGuideReason: "No matching FOEN warning-map guide is yet in the Data catalog.", + }, + { + group: "Hazard and discovery feeds", + name: "Austria eHYD flood stages", + role: "Official flood-warning stages at mapped hydrographic stations.", + availability: "Configured; raw water levels do not score as warnings.", + officialUrl: "https://ehyd.gv.at/", + noGuideReason: "No matching eHYD flood-stage guide is yet in the Data catalog.", + }, + { + group: "Hazard and discovery feeds", + name: "NASA EONET", + role: "Recent wildfire and volcano event context.", + availability: "Environment-gated, non-blocking, and not a local warning source.", + officialUrl: "https://eonet.gsfc.nasa.gov/", + noGuideReason: "No matching NASA EONET guide is yet in the Data catalog.", + }, + { + group: "Hazard and discovery feeds", + name: "Copernicus European Drought Observatory", + role: "Dekadal agricultural and ecosystem drought context.", + availability: "Environment-gated; not an immediate emergency warning.", + officialUrl: "https://drought.emergency.copernicus.eu/", + noGuideReason: "No matching EDO drought-indicator guide is yet in the Data catalog.", + }, + { + group: "Hazard and discovery feeds", + name: "UK FCDO travel advice", + role: "Whole-country security and travel-advice context.", + availability: "Environment-gated; the text does not imply sub-country warning geometry.", + officialUrl: "https://www.gov.uk/foreign-travel-advice", + noGuideReason: "No matching FCDO advice guide is yet in the Data catalog.", + }, + { + group: "Local conditions", + name: "Open-Meteo forecasts and Copernicus CAMS", + role: "Hourly weather, modeled air-quality, dust and UV, plus nearby marine forecasts.", + availability: "Configured but restricted to accepted noncommercial use and local-conditions activation; forecasts are context, not observations or warnings.", + coverage: "Weather, air-quality, and marine products; marine applies only to reviewed coastal destinations.", + officialUrl: "https://open-meteo.com/", + additionalUrls: [{ label: "Copernicus Atmosphere Monitoring Service", href: "https://atmosphere.copernicus.eu/" }], + noGuideReason: "No matching Open-Meteo forecast guide is yet in the Data catalog; the CAMS model is accessed through Open-Meteo here.", + }, + { + group: "Local conditions", + name: "MET Norway Locationforecast", + role: "Fallback point weather forecast.", + availability: "Configured when local conditions are activated; forecast values are not observations or official warnings.", + officialUrl: "https://api.met.no/weatherapi/locationforecast/2.0/documentation", + additionalUrls: [{ label: "MET Norway", href: "https://www.met.no/en" }], + guideId: "met-norway-locationforecast", + }, + { + group: "Local conditions", + name: "NOAA Aviation Weather Center METAR and station cache", + role: "Airport weather observations at reviewed representative stations; the pinned station cache maps airports to destinations.", + availability: "Configured when local conditions are activated; airport readings are not destination-wide weather conditions or aviation advice.", + officialUrl: "https://aviationweather.gov/data/api/", + additionalUrls: [{ label: "Aviation Weather Center", href: "https://aviationweather.gov/" }], + noGuideReason: "No matching Aviation Weather Center METAR or station-cache guide is yet in the Data catalog.", + }, + { + group: "Local conditions", + name: "IPMA weather observations and seismic records", + role: "Portuguese station observations and recent regional earthquake context.", + availability: "Configured but restricted to accepted noncommercial use and local-conditions activation; measurements do not establish warning coverage.", + officialUrl: "https://api.ipma.pt/", + noGuideReason: "No matching IPMA station or regional seismic guide is yet in the Data catalog.", + }, + { + group: "Local conditions", + name: "IGN regional earthquakes", + role: "Candidate Spanish regional earthquake context.", + availability: "Gated and not connected until RSS event time, corrections, and deletion lifecycle are verified.", + officialUrl: "https://www.ign.es/web/social-rss", + noGuideReason: "Activation review is incomplete, so a matching guide is pending.", + }, + { + group: "Local conditions", + name: "ARSO hydrology", + role: "Provisional Slovenian river observations at reviewed stations.", + availability: "Configured for local conditions; reference-level crossings are factual context, not official flood warnings.", + officialUrl: "https://www.arso.gov.si/vode/podatki/hidro_podatki_xml.html", + noGuideReason: "No matching ARSO hydrology guide is yet in the Data catalog.", + }, + { + group: "Local conditions", + name: "Ireland OPW water levels", + role: "Provisional Irish gauge observations relative to local gauge zero.", + availability: "Configured for local conditions; unchecked station data are not flood warnings.", + officialUrl: "https://waterlevel.ie/page/api/", + additionalUrls: [{ label: "OPW waterlevel.ie", href: "https://waterlevel.ie/" }], + noGuideReason: "No matching OPW water-level guide is yet in the Data catalog.", + }, + { + group: "Local conditions", + name: "Rijkswaterstaat Waterdata", + role: "Dutch station water levels with quality-code and NAP-datum checks; pinned station metadata maps locations.", + availability: "Configured for local conditions; observations are not flood warnings.", + officialUrl: "https://rijkswaterstaatdata.nl/waterdata/", + noGuideReason: "No matching Rijkswaterstaat Waterdata guide is yet in the Data catalog.", + }, + { + group: "Local conditions", + name: "VMM Waterinfo", + role: "Candidate Belgian river-level observations.", + availability: "Gated: reviewed live group yielded rainfall, while water-level groups, quality codes, mappings, and HIC access remain unapproved.", + officialUrl: "https://www.waterinfo.vlaanderen.be/default.aspx?path=Public%2FOver+waterinfo%2FFAQ+open+data", + noGuideReason: "Activation and exact dataset review remain incomplete.", + }, + { + group: "Local conditions", + name: "Lithuanian Hydrometeorological Service", + role: "Candidate river and station observations.", + availability: "Gated until exact station mappings and vertical-reference metadata are reviewed.", + officialUrl: "https://api.meteo.lt/", + noGuideReason: "Activation and exact dataset review remain incomplete.", + }, + { + group: "Local conditions", + name: "Finnish Environment Institute SYKE", + role: "Candidate hydrological station observations.", + availability: "Gated pending OData fixture, quality flag, datum, and station-mapping review.", + officialUrl: "https://www.syke.fi/en/environmental-data/open-web-services/environmental-data-apis", + noGuideReason: "Activation and exact dataset review remain incomplete.", + }, + { + group: "Local conditions", + name: "Cyprus air-quality network", + role: "Candidate air-quality station observations.", + availability: "Gated pending local-time, instrument-flag, and station-mapping review.", + officialUrl: "https://www.airquality.dli.mlsi.gov.cy/", + noGuideReason: "Activation and exact dataset review remain incomplete.", + }, + { + group: "Local conditions", + name: "Bulgarian NIMH hydrology", + role: "Candidate daily hydrological observations.", + availability: "Gated pending timestamp, reuse, and station-area review; daily values cannot imply current flood warnings.", + officialUrl: "https://info.meteo.bg/openData/", + noGuideReason: "Activation and exact dataset review remain incomplete.", + }, + { + group: "Local conditions", + name: "Fintraffic Digitraffic", + role: "Current accident-associated road closures matched by official geometry.", + availability: "Configured for local conditions; nearby context is not complete disruption coverage or route advice.", + officialUrl: "https://www.digitraffic.fi/en/road-traffic/", + noGuideReason: "No matching Digitraffic traffic-announcement guide is yet in the Data catalog.", + }, + { + group: "Local conditions", + name: "SMHI water-shortage information", + role: "Candidate Swedish water-shortage messages.", + availability: "Gated until informational-message validity and reuse lifecycle fixtures pass review.", + officialUrl: "https://opendata-download-warnings.smhi.se/ibww/api/version/1", + noGuideReason: "Activation and exact dataset review remain incomplete.", + }, + { + group: "Local conditions", + name: "Krisinformation infrastructure notices", + role: "Official Swedish infrastructure and crisis notices, reduced to fixed factual categories.", + availability: "Configured for local conditions; regional notices do not prove a destination-wide interruption.", + officialUrl: "https://www.krisinformation.se/", + noGuideReason: "No matching Krisinformation notice API guide is yet in the Data catalog.", + }, + { + group: "Local conditions", + name: "NDW open traffic data", + role: "Dutch serious road incidents and complete closures intersecting destinations.", + availability: "Configured for local conditions; not route advice.", + officialUrl: "https://opendata.ndw.nu/", + noGuideReason: "No matching NDW incident-feed guide is yet in the Data catalog.", + }, + { + group: "Local conditions", + name: "Autobahn GmbH traffic information", + role: "German motorway closures and warnings intersecting destinations.", + availability: "Configured but restricted to accepted operator use and local-conditions activation; not complete transport monitoring.", + officialUrl: "https://www.autobahn.de/", + noGuideReason: "No matching Autobahn traffic API guide is yet in the Data catalog.", + }, + { + group: "Local conditions", + name: "Electricity Authority of Cyprus", + role: "Candidate current and scheduled power interruptions by district.", + availability: "Gated: five public district pages exceeded the reviewed eight-second source budget.", + officialUrl: "https://www.eac.com.cy/EN/RegulatedActivities/Distribution/PowerInterruptions/Pages/Faultsandscheduledinterruptions.aspx?District=0", + noGuideReason: "The product's source gate failed; an exact guide needs separate access review.", + }, + { + group: "Local conditions", + name: "Enemalta outages", + role: "Candidate Maltese current and planned electricity interruptions.", + availability: "Gated: the official planned-outage response exceeded the approved 512 KiB limit.", + officialUrl: "https://www.enemalta.com.mt/planned-power-cuts/", + noGuideReason: "The product's source gate failed; an exact guide needs separate access review.", + }, + { + group: "Local conditions", + name: "PSE Energy Compass", + role: "Polish national electricity-use recommendations, not local outages.", + availability: "Configured for local conditions; advice describes system-wide conditions only.", + officialUrl: "https://www.energetycznykompas.pl/", + noGuideReason: "No matching PSE Energy Compass guide is yet in the Data catalog.", + }, + { + group: "Pinned inputs", + name: "GeoNames destination gazetteer", + role: "Pinned place names and representative coordinates for the 679-destination catalog.", + availability: "Pinned catalog input, not a live feed; regional matching areas are TravelCanary approximations.", + officialUrl: "https://download.geonames.org/export/dump/", + evidenceUrl: "https://github.com/hypertrial/travelcanary/blob/1ff2fdfa06b38880175a03aed10cfe60352bd0f6/data/review-inputs/europe-expansion-catalog.json", + noGuideReason: "No matching GeoNames dump guide is yet in the Data catalog.", + }, + { + group: "Pinned inputs", + name: "Eurostat GISCO NUTS 2024 geometry", + role: "Pinned NUTS-2 region mapping used to match destinations to MeteoAlarm warnings.", + availability: "Pinned geographic input; a regional polygon alone does not establish warning coverage.", + officialUrl: "https://gisco-services.ec.europa.eu/distribution/v2/nuts/nuts-2024-files.html", + evidenceUrl: "https://github.com/hypertrial/travelcanary/blob/1ff2fdfa06b38880175a03aed10cfe60352bd0f6/data/catalog-metadata.json", + noGuideReason: "The existing Eurostat statistics guide is a different product; no GISCO geometry guide is yet available.", + }, + { + group: "Pinned inputs", + name: "EAWS avalanche regions", + role: "Pinned avalanche micro-region geometry used for candidate bulletin-to-destination intersections.", + availability: "Pinned geographic review input; new region matches remain gated until bulletin and seasonal scope review.", + officialUrl: "https://eaws.gitlab.io/eaws-regions/micro-regions.geojson", + evidenceUrl: "https://github.com/hypertrial/travelcanary/blob/1ff2fdfa06b38880175a03aed10cfe60352bd0f6/data/review-inputs/eaws-expansion-review.json", + noGuideReason: "No matching EAWS regions guide is yet in the Data catalog.", + }, + { + group: "Pinned inputs", + name: "Smithsonian Global Volcanism Program Holocene volcanoes", + role: "Pinned volcano locations used to decide where volcanic hazard checks apply.", + availability: "Pinned reference data, not a live eruption feed.", + officialUrl: "https://volcano.si.edu/gvp_webservices.cfm", + evidenceUrl: "https://github.com/hypertrial/travelcanary/blob/1ff2fdfa06b38880175a03aed10cfe60352bd0f6/data/review-inputs/gvp-holocene.json", + guideId: "smithsonian-gvp-volcanoes", + }, + // National warning partitions are a separate, explicitly reviewed matrix. + // Each country entry below names every configured, gated, or blocked system. + { + group: "National warnings", + name: "Andorra (AD) warning systems", + role: "Official national and direct warning sources reviewed for Andorra.", + availability: "National civil-alert partition not enabled; 1 active, 2 gated systems listed.", + details: "MeteoAlarm keyless Atom warning feed (active; MeteoAlarm primary); MeteoAlarm authenticated EDR recovery (credential-gated; MeteoAlarm fallback); Andorra weather alerts (evidence-gated; official link/review only).", + officialUrl: "https://www.meteoalarm.org/", + additionalUrls: [ + { label: "Andorra weather alerts", href: "https://www.meteo.ad/en/Alerts" }, + ], + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Albania (AL) warning systems", + role: "Official national and direct warning sources reviewed for Albania.", + availability: "National civil-alert partition not enabled; 1 gated system listed.", + details: "Institute of Geosciences (evidence-gated; official link/review only).", + officialUrl: "https://geo.edu.al/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Austria (AT) warning systems", + role: "Official national and direct warning sources reviewed for Austria.", + availability: "National civil-alert partition enabled; 1 active system listed.", + details: "AT-Alert (active; national warning feed).", + officialUrl: "https://warnung.at-alert.at/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Bosnia and Herzegovina (BA) warning systems", + role: "Official national and direct warning sources reviewed for Bosnia and Herzegovina.", + availability: "National civil-alert partition not enabled; 3 gated systems listed.", + details: "MeteoAlarm keyless Atom warning feed (evidence-gated; official link/review only); MeteoAlarm authenticated EDR recovery (evidence-gated; official link/review only); Federal Hydrometeorological Institute (evidence-gated; official link/review only).", + officialUrl: "https://www.meteoalarm.org/", + additionalUrls: [ + { label: "Federal Hydrometeorological Institute", href: "https://www.fhmzbih.gov.ba/" }, + ], + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Belgium (BE) warning systems", + role: "Official national and direct warning sources reviewed for Belgium.", + availability: "National civil-alert partition not enabled; 1 gated system listed.", + details: "BE-Alert (evidence-gated; official link/review only).", + officialUrl: "https://www.be-alert.be/en/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Bulgaria (BG) warning systems", + role: "Official national and direct warning sources reviewed for Bulgaria.", + availability: "National civil-alert partition not enabled; 1 gated system listed.", + details: "BG-ALERT (evidence-gated; official link/review only).", + officialUrl: "https://bg-alert.bg/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Belarus (BY) warning systems", + role: "Official national and direct warning sources reviewed for Belarus.", + availability: "National civil-alert partition not enabled; 1 gated system listed.", + details: "Belhydromet weather information (evidence-gated; official link/review only).", + officialUrl: "https://pogoda.by/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Switzerland (CH) warning systems", + role: "Official national and direct warning sources reviewed for Switzerland.", + availability: "National civil-alert partition not enabled; 1 blocked system listed.", + details: "Alertswiss (blocked; official link/review only).", + officialUrl: "https://www.alert.swiss/en/home.html", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Cyprus (CY) warning systems", + role: "Official national and direct warning sources reviewed for Cyprus.", + availability: "National civil-alert partition not enabled; 1 gated system listed.", + details: "Cyprus public warning system (evidence-gated; official link/review only).", + officialUrl: "https://www.gov.cy/moi/en/civil-defence/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Czechia (CZ) warning systems", + role: "Official national and direct warning sources reviewed for Czechia.", + availability: "National civil-alert partition enabled; 1 active system listed.", + details: "CHMI hydrology and flash-flood risk (active; national warning feed).", + officialUrl: "https://opendata.chmi.cz/hydrology/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Germany (DE) warning systems", + role: "Official national and direct warning sources reviewed for Germany.", + availability: "National civil-alert partition enabled; 2 active, 1 gated systems listed.", + details: "LHP flood warnings (active; national warning feed); BBK civil-protection RSS (evidence-gated; official link/review only); DWD direct CAP recovery (active; MeteoAlarm fallback).", + officialUrl: "https://www.hochwasserzentralen.de/", + additionalUrls: [ + { label: "BBK civil-protection RSS", href: "https://www.bbk.bund.de/DE/Warnung-Vorsorge/Warn-App-NINA/warn-app-nina_node.html" }, + { label: "DWD direct CAP recovery", href: "https://opendata.dwd.de/weather/alerts/cap/COMMUNEUNION_EVENT_STAT/" }, + ], + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Denmark (DK) warning systems", + role: "Official national and direct warning sources reviewed for Denmark.", + availability: "National civil-alert partition not enabled; 1 gated system listed.", + details: "S!RENEN (evidence-gated; official link/review only).", + officialUrl: "https://www.sirenen.dk/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Estonia (EE) warning systems", + role: "Official national and direct warning sources reviewed for Estonia.", + availability: "National civil-alert partition not enabled; 1 blocked system listed.", + details: "EE-ALARM (blocked; official link/review only).", + officialUrl: "https://www.olevalmis.ee/en/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Spain (ES) warning systems", + role: "Official national and direct warning sources reviewed for Spain.", + availability: "National civil-alert partition enabled; 2 active systems listed.", + details: "Catalonia civil-protection plans (active; national warning feed); AEMET direct CAP warnings (active; MeteoAlarm fallback).", + officialUrl: "https://interior.gencat.cat/ca/arees_dactuacio/proteccio_civil/", + additionalUrls: [ + { label: "AEMET direct CAP warnings", href: "https://www.aemet.es/es/rss_info/avisos/esp" }, + ], + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Finland (FI) warning systems", + role: "Official national and direct warning sources reviewed for Finland.", + availability: "National civil-alert partition not enabled; 1 active, 1 blocked systems listed.", + details: "Finland public warning system (blocked; official link/review only); FMI CAP warnings (active; MeteoAlarm fallback).", + officialUrl: "https://www.suomi.fi/guides/preparedness", + additionalUrls: [ + { label: "FMI CAP warnings", href: "https://alerts.fmi.fi/cap/profile/current/" }, + ], + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "France (FR) warning systems", + role: "Official national and direct warning sources reviewed for France.", + availability: "National civil-alert partition enabled; 1 active, 1 gated systems listed.", + details: "FR-Alert (active; national warning feed); Météo-France Vigilance API (credential-gated; official link/review only).", + officialUrl: "https://fr-alert.gouv.fr/les-alertes", + additionalUrls: [ + { label: "Météo-France Vigilance API", href: "https://www.data.gouv.fr/dataservices/api-bulletin-vigilance" }, + ], + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "United Kingdom (GB) warning systems", + role: "Official national and direct warning sources reviewed for United Kingdom.", + availability: "National civil-alert partition enabled; 1 active, 5 gated systems listed.", + details: "England flood warnings (active; national warning feed); MeteoAlarm keyless Atom warning feed (evidence-gated; official link/review only); MeteoAlarm authenticated EDR recovery (evidence-gated; official link/review only); Met Office UK weather warnings (evidence-gated; official link/review only); National Severe Weather Warning Service (credential-gated; national warning feed); Wales flood warnings (credential-gated; national warning feed).", + officialUrl: "https://check-for-flooding.service.gov.uk/", + additionalUrls: [ + { label: "MeteoAlarm keyless Atom warning feed", href: "https://www.meteoalarm.org/" }, + { label: "Met Office UK weather warnings", href: "https://weather.metoffice.gov.uk/warnings-and-advice/uk-warnings" }, + { label: "National Severe Weather Warning Service", href: "https://www.metoffice.gov.uk/weather/warnings-and-advice/uk-warnings" }, + { label: "Wales flood warnings", href: "https://naturalresources.wales/flooding/check-flood-warnings/?lang=en" }, + ], + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Greece (GR) warning systems", + role: "Official national and direct warning sources reviewed for Greece.", + availability: "National civil-alert partition not enabled; 1 gated system listed.", + details: "112 Greece (evidence-gated; official link/review only).", + officialUrl: "https://civilprotection.gov.gr/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Croatia (HR) warning systems", + role: "Official national and direct warning sources reviewed for Croatia.", + availability: "National civil-alert partition not enabled; 1 active, 1 blocked systems listed.", + details: "SRUUK (blocked; official link/review only); DHMZ direct CAP warnings (active; MeteoAlarm fallback).", + officialUrl: "https://civilna-zastita.gov.hr/", + additionalUrls: [ + { label: "DHMZ direct CAP warnings", href: "https://meteo.hr/proizvodi.php?section=podaci¶m=xml_korisnici" }, + ], + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Hungary (HU) warning systems", + role: "Official national and direct warning sources reviewed for Hungary.", + availability: "National civil-alert partition not enabled; 1 blocked system listed.", + details: "VÉSZ (blocked; official link/review only).", + officialUrl: "https://www.katasztrofavedelem.hu/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Ireland (IE) warning systems", + role: "Official national and direct warning sources reviewed for Ireland.", + availability: "National civil-alert partition not enabled; 1 active, 1 blocked systems listed.", + details: "Ireland public warning system (blocked; official link/review only); Met Eireann warnings (active; MeteoAlarm fallback).", + officialUrl: "https://www.gov.ie/en/department-of-defence/organisation-information/office-of-emergency-planning/", + additionalUrls: [ + { label: "Met Eireann warnings", href: "https://www.met.ie/warnings-today.html" }, + ], + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Iceland (IS) warning systems", + role: "Official national and direct warning sources reviewed for Iceland.", + availability: "National civil-alert partition not enabled; 1 active, 2 gated systems listed.", + details: "MeteoAlarm keyless Atom warning feed (active; MeteoAlarm primary); MeteoAlarm authenticated EDR recovery (credential-gated; MeteoAlarm fallback); Icelandic Met Office (evidence-gated; official link/review only).", + officialUrl: "https://www.meteoalarm.org/", + additionalUrls: [ + { label: "Icelandic Met Office", href: "https://en.vedur.is/" }, + ], + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Italy (IT) warning systems", + role: "Official national and direct warning sources reviewed for Italy.", + availability: "National civil-alert partition enabled; 1 active, 1 gated, 1 blocked systems listed.", + details: "IT-alert (blocked; official link/review only); National hydrogeological and hydraulic bulletin (active; national warning feed); Italian volcanic warnings and restrictions (evidence-gated; official link/review only).", + officialUrl: "https://www.it-alert.gov.it/en/", + additionalUrls: [ + { label: "National hydrogeological and hydraulic bulletin", href: "https://mappe.protezionecivile.gov.it/it/mappe-rischi/bollettino-di-criticita/" }, + { label: "Italian volcanic warnings and restrictions", href: "https://rischi.protezionecivile.gov.it/it/vulcanico/vulcani-italia/stromboli/" }, + ], + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Liechtenstein (LI) warning systems", + role: "Official national and direct warning sources reviewed for Liechtenstein.", + availability: "National civil-alert partition not enabled; 1 gated system listed.", + details: "Liechtenstein government information (evidence-gated; official link/review only).", + officialUrl: "https://www.llv.li/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Lithuania (LT) warning systems", + role: "Official national and direct warning sources reviewed for Lithuania.", + availability: "National civil-alert partition not enabled; 1 gated system listed.", + details: "LT72 (evidence-gated; official link/review only).", + officialUrl: "https://lt72.lt/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Luxembourg (LU) warning systems", + role: "Official national and direct warning sources reviewed for Luxembourg.", + availability: "National civil-alert partition enabled; 1 active system listed.", + details: "LU-Alert (active; national warning feed).", + officialUrl: "https://data.public.lu/fr/datasets/alertes-du-systeme-lu-alert/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Latvia (LV) warning systems", + role: "Official national and direct warning sources reviewed for Latvia.", + availability: "National civil-alert partition enabled; 1 active, 1 gated systems listed.", + details: "Latvia public warning system (evidence-gated; official link/review only); LVĢMC hydrological warnings (active; national warning feed).", + officialUrl: "https://www.vugd.gov.lv/en", + additionalUrls: [ + { label: "LVĢMC hydrological warnings", href: "https://data.gov.lv/dati/dataset/hidrometeorologiskie-bridinajumi" }, + ], + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Monaco (MC) warning systems", + role: "Official national and direct warning sources reviewed for Monaco.", + availability: "National civil-alert partition not enabled; 1 gated system listed.", + details: "Monaco government information (evidence-gated; official link/review only).", + officialUrl: "https://www.gouv.mc/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Moldova (MD) warning systems", + role: "Official national and direct warning sources reviewed for Moldova.", + availability: "National civil-alert partition not enabled; 3 gated systems listed.", + details: "MeteoAlarm keyless Atom warning feed (evidence-gated; official link/review only); MeteoAlarm authenticated EDR recovery (evidence-gated; official link/review only); Moldova weather warnings (evidence-gated; official link/review only).", + officialUrl: "https://www.meteoalarm.org/", + additionalUrls: [ + { label: "Moldova weather warnings", href: "https://www.meteo.md/index.php/ro/weather/current-warnings/" }, + ], + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Montenegro (ME) warning systems", + role: "Official national and direct warning sources reviewed for Montenegro.", + availability: "National civil-alert partition not enabled; 3 gated systems listed.", + details: "MeteoAlarm keyless Atom warning feed (evidence-gated; official link/review only); MeteoAlarm authenticated EDR recovery (evidence-gated; official link/review only); Montenegro Hydrometeorological and Seismological Service (evidence-gated; official link/review only).", + officialUrl: "https://www.meteoalarm.org/", + additionalUrls: [ + { label: "Montenegro Hydrometeorological and Seismological Service", href: "https://www.meteo.co.me/" }, + ], + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "North Macedonia (MK) warning systems", + role: "Official national and direct warning sources reviewed for North Macedonia.", + availability: "National civil-alert partition not enabled; 3 gated systems listed.", + details: "MeteoAlarm keyless Atom warning feed (evidence-gated; official link/review only); MeteoAlarm authenticated EDR recovery (evidence-gated; official link/review only); North Macedonia Hydrometeorological Service (evidence-gated; official link/review only).", + officialUrl: "https://www.meteoalarm.org/", + additionalUrls: [ + { label: "North Macedonia Hydrometeorological Service", href: "https://uhmr.gov.mk/" }, + ], + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Malta (MT) warning systems", + role: "Official national and direct warning sources reviewed for Malta.", + availability: "National civil-alert partition not enabled; 1 blocked system listed.", + details: "Malta public warning system (blocked; official link/review only).", + officialUrl: "https://cps.gov.mt/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Netherlands (NL) warning systems", + role: "Official national and direct warning sources reviewed for Netherlands.", + availability: "National civil-alert partition not enabled; 1 gated system listed.", + details: "NL-Alert (evidence-gated; official link/review only).", + officialUrl: "https://www.nl-alert.nl/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Norway (NO) warning systems", + role: "Official national and direct warning sources reviewed for Norway.", + availability: "National civil-alert partition enabled; 2 active, 3 gated systems listed.", + details: "MET Norway MetAlerts (active; national warning feed); NVE flood warnings (active; national warning feed); MeteoAlarm keyless Atom warning feed (evidence-gated; official link/review only); MeteoAlarm authenticated EDR recovery (evidence-gated; official link/review only); Varsom natural hazard warnings (evidence-gated; official link/review only).", + officialUrl: "https://www.met.no/en/weather-and-climate/text-forecast-and-warnings", + additionalUrls: [ + { label: "NVE flood warnings", href: "https://www.varsom.no/en/flood-and-landslide-warning-service/" }, + { label: "MeteoAlarm keyless Atom warning feed", href: "https://www.meteoalarm.org/" }, + { label: "Varsom natural hazard warnings", href: "https://www.varsom.no/en/" }, + ], + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Poland (PL) warning systems", + role: "Official national and direct warning sources reviewed for Poland.", + availability: "National civil-alert partition enabled; 1 active system listed.", + details: "IMGW hydrology warnings (active; national warning feed).", + officialUrl: "https://hydro.imgw.pl/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Portugal (PT) warning systems", + role: "Official national and direct warning sources reviewed for Portugal.", + availability: "National civil-alert partition not enabled; 1 active, 2 gated, 1 blocked systems listed.", + details: "IPMA weather warnings (active; MeteoAlarm fallback); PROCIV (blocked; official link/review only); Azores Civil Protection alerts (evidence-gated; national warning feed); ANEPC operational incidents (evidence-gated; national warning feed).", + officialUrl: "https://www.ipma.pt/en/otempo/prev-sam/", + additionalUrls: [ + { label: "PROCIV", href: "https://prociv.gov.pt/" }, + { label: "Azores Civil Protection alerts", href: "https://www.prociv.azores.gov.pt/alertas/" }, + { label: "ANEPC operational incidents", href: "https://prociv.gov.pt/pt/ocorrencias/" }, + ], + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Romania (RO) warning systems", + role: "Official national and direct warning sources reviewed for Romania.", + availability: "National civil-alert partition not enabled; 1 blocked system listed.", + details: "RO-ALERT (blocked; official link/review only).", + officialUrl: "https://www.mai.gov.ro/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Serbia (RS) warning systems", + role: "Official national and direct warning sources reviewed for Serbia.", + availability: "National civil-alert partition not enabled; 3 gated systems listed.", + details: "MeteoAlarm keyless Atom warning feed (evidence-gated; official link/review only); MeteoAlarm authenticated EDR recovery (evidence-gated; official link/review only); Serbia hydrometeorological warnings (evidence-gated; official link/review only).", + officialUrl: "https://www.meteoalarm.org/", + additionalUrls: [ + { label: "Serbia hydrometeorological warnings", href: "https://www.hidmet.gov.rs/latin/upozorenja/index.php" }, + ], + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Sweden (SE) warning systems", + role: "Official national and direct warning sources reviewed for Sweden.", + availability: "National civil-alert partition enabled; 1 active, 1 gated systems listed.", + details: "Krisinformation (active; national warning feed); SMHI direct warnings (evidence-gated; official link/review only).", + officialUrl: "https://www.krisinformation.se/en", + additionalUrls: [ + { label: "SMHI direct warnings", href: "https://opendata-download-warnings.smhi.se/ibww/api/version/1" }, + ], + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Slovenia (SI) warning systems", + role: "Official national and direct warning sources reviewed for Slovenia.", + availability: "National civil-alert partition not enabled; 1 gated system listed.", + details: "Slovenia public warning system (evidence-gated; official link/review only).", + officialUrl: "https://www.gov.si/en/state-authorities/bodies-within-ministries/administration-for-civil-protection-and-disaster-relief/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Slovakia (SK) warning systems", + role: "Official national and direct warning sources reviewed for Slovakia.", + availability: "National civil-alert partition not enabled; 1 gated, 1 blocked systems listed.", + details: "Slovakia public warning system (blocked; official link/review only); Crisis-management REST service (credential-gated; national warning feed).", + officialUrl: "https://www.minv.sk/", + additionalUrls: [ + { label: "Crisis-management REST service", href: "https://portal.minv.sk/wps/esispz-api/docs/index.html" }, + ], + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "San Marino (SM) warning systems", + role: "Official national and direct warning sources reviewed for San Marino.", + availability: "National civil-alert partition not enabled; 1 gated system listed.", + details: "San Marino civil protection orders (evidence-gated; official link/review only).", + officialUrl: "https://www.gov.sm/pub1/GovSM/Circolari-e-Ordinanze/Ordinanze-Protezione-Civile.html", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Türkiye (TR) warning systems", + role: "Official national and direct warning sources reviewed for Türkiye.", + availability: "National civil-alert partition not enabled; 1 gated system listed.", + details: "Türkiye meteorological information (evidence-gated; official link/review only).", + officialUrl: "https://www.mgm.gov.tr/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Vatican City (VA) warning systems", + role: "Official national and direct warning sources reviewed for Vatican City.", + availability: "National civil-alert partition not enabled; 1 gated system listed.", + details: "Vatican City State information (evidence-gated; official link/review only).", + officialUrl: "https://www.vaticanstate.va/en/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + { + group: "National warnings", + name: "Kosovo (XK) warning systems", + role: "Official national and direct warning sources reviewed for Kosovo.", + availability: "National civil-alert partition not enabled; 1 gated system listed.", + details: "Kosovo Hydrometeorological Institute (evidence-gated; official link/review only).", + officialUrl: "https://ihmk-rks.net/", + noGuideReason: "No matching national warning feed guide is yet in the Data catalog; each listed system has separate access and reuse terms.", + }, + ], +} as const; diff --git a/src/content/site-copy.ts b/src/content/site-copy.ts index d8670fc..0eca670 100644 --- a/src/content/site-copy.ts +++ b/src/content/site-copy.ts @@ -8,6 +8,7 @@ export const siteCopy = { primaryNavigationLabel: "Primary", mobileNavigationLabel: "Mobile Primary", datasetsNavigationLabel: "Datasets", + appsNavigationLabel: "Apps", collectionsNavigationLabel: "Build Paths", contributeLabel: "Contribute", feedbackLabel: "Send Feedback", @@ -34,6 +35,31 @@ export const siteCopy = { `© ${year} Trilemma Foundation. All rights reserved.`, } as const; +export const appsCopy = { + title: "Apps", + description: + "Explore six data-powered projects, their source code, and the data behind them.", + detailsLabel: "Explore App", + sourcesTitle: "Data Sources", + sourceDisclosure: + "This is a reviewed source inventory, not a live feed-health report. Availability depends on each app's configuration and the provider's current service.", + sourceRoleLabel: "Role", + sourceAvailabilityLabel: "Availability", + sourceCoverageLabel: "Coverage", + officialSourceLabel: "Official Source", + sourceEvidenceLabel: "Project Source Evidence", + guideLabel: "Dataset Guide", + reviewedLabel: "Source Map Reviewed", + statusLabels: { + alpha: "Alpha", + beta: "Beta", + archived: "Archived", + }, + liveAppLabel: "Live App", + archiveLabel: "Archive", + sourceCodeLabel: "Source Code", +} as const; + export const catalogCopy = { heroTitle: "Choose a Dataset. Build a Microproduct.", heroTrust: (count: number, verified: string) => diff --git a/src/lib/nav.test.ts b/src/lib/nav.test.ts index 499a4d2..6810dcb 100644 --- a/src/lib/nav.test.ts +++ b/src/lib/nav.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import * as nav from "./nav"; import { + isAppsPath, isCollectionsPath, isContributePath, isDatasetsPath, @@ -14,17 +15,27 @@ describe("primary navigation current paths", () => { expect(isDatasetsPath("/themes/environment-hazards")).toBe(true); expect(isDatasetsPath("/themes/environment-hazards/")).toBe(true); expect(isDatasetsPath("/collections")).toBe(false); + expect(isDatasetsPath("/apps")).toBe(false); expect(isDatasetsPath("/compare")).toBe(false); expect(isDatasetsPath("/contribute")).toBe(false); }); it("scopes the other primary destinations to their routes", () => { + expect(isAppsPath("/apps")).toBe(true); + expect(isAppsPath("/apps/")).toBe(true); + expect(isAppsPath("/apps/travelcanary")).toBe(true); + expect(isAppsPath("/apps/travelcanary/")).toBe(true); + expect(isAppsPath("/app")).toBe(false); + expect(isAppsPath("/apps-extra")).toBe(false); + expect(isAppsPath("/datasets/apps")).toBe(false); expect(isCollectionsPath("/collections")).toBe(true); expect(isCollectionsPath("/collections/")).toBe(true); expect(isCollectionsPath("/collections/first-builds")).toBe(true); expect(isContributePath("/contribute")).toBe(true); expect(isContributePath("/contribute/")).toBe(true); expect(isCollectionsPath("/")).toBe(false); + expect(isCollectionsPath("/apps")).toBe(false); + expect(isContributePath("/apps")).toBe(false); expect(isContributePath("/collections")).toBe(false); }); diff --git a/src/lib/nav.ts b/src/lib/nav.ts index 3d4995d..b8fc4c0 100644 --- a/src/lib/nav.ts +++ b/src/lib/nav.ts @@ -1,4 +1,4 @@ -import { COLLECTIONS_PATH, CONTRIBUTE_APP_PATH } from "./seo"; +import { APPS_PATH, COLLECTIONS_PATH, CONTRIBUTE_APP_PATH } from "./seo"; function normalizePath(pathname: string): string { return pathname.length > 1 && pathname.endsWith("/") @@ -21,6 +21,10 @@ export function isCollectionsPath(pathname: string): boolean { return isSectionPath(pathname, COLLECTIONS_PATH); } +export function isAppsPath(pathname: string): boolean { + return isSectionPath(pathname, APPS_PATH); +} + export function isContributePath(pathname: string): boolean { return isSectionPath(pathname, CONTRIBUTE_APP_PATH); } diff --git a/src/lib/seo.ts b/src/lib/seo.ts index 91f730e..cffed2d 100644 --- a/src/lib/seo.ts +++ b/src/lib/seo.ts @@ -15,6 +15,7 @@ export const GITHUB_REPO_URL = "https://github.com/TrilemmaFoundation/data"; export const CONTRIBUTE_URL = `${GITHUB_REPO_URL}/blob/main/CONTRIBUTING.md`; export const CONTRIBUTE_APP_PATH = "/contribute"; export const COLLECTIONS_PATH = "/collections"; +export const APPS_PATH = "/apps"; export const FEEDBACK_URL = `${GITHUB_REPO_URL}/issues/new?template=usability.yml`; export const DATASET_ISSUE_URL = `${GITHUB_REPO_URL}/issues/new?template=dataset.yml`; @@ -26,6 +27,10 @@ export function collectionPath(id: string): string { return `/collections/${id}`; } +export function appPath(slug: string): string { + return `${APPS_PATH}/${slug}`; +} + export function pageSocialMetadata(path: string, title: string, description: string) { return { title,