diff --git a/e2e/apps.spec.ts b/e2e/apps.spec.ts index f8979d6..27a3320 100644 --- a/e2e/apps.spec.ts +++ b/e2e/apps.spec.ts @@ -64,17 +64,18 @@ test("cards and details expose only the intended external actions", async ({ pag ["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"], + ["Archive", "https://stackingsats.org/", "https://github.com/hypertrial/stacksats", "https://bitview.space/"], ] as const; const statuses = [undefined, undefined, "Beta", "Alpha", "Alpha", "Archived"] as const; - for (const [index, [action, destination, source]] of expected.entries()) { + for (const [index, [action, destination, source, brkUrl]] 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); + await expect(links).toHaveCount((action ? 2 : 1) + (brkUrl ? 1 : 0)); if (action) await expect(links.first()).toHaveAttribute("href", destination!); + if (brkUrl) await expect(links.nth(1)).toHaveAttribute("href", brkUrl); await expect(links.last()).toHaveAttribute("href", source); for (const link of await links.all()) { await expect(link).toHaveAttribute("rel", /noopener.*noreferrer/); @@ -90,6 +91,7 @@ test("cards and details expose only the intended external actions", async ({ pag await page.goto("/apps/stackingsats"); await expect(page.getByRole("link", { name: /Archive/ })).toHaveAttribute("href", "https://stackingsats.org/"); + await expect(page.getByRole("link", { name: /Explore BRK Data/ })).toHaveAttribute("href", "https://bitview.space/"); await expect(page.getByRole("link", { name: /Live App/ })).toHaveCount(0); await expect(page.getByText(/historical|archiv/i).first()).toBeVisible(); }); diff --git a/src/components/AppActions.tsx b/src/components/AppActions.tsx index 3190f45..031ef5a 100644 --- a/src/components/AppActions.tsx +++ b/src/components/AppActions.tsx @@ -6,6 +6,7 @@ export function AppActions({ app }: { app: AppEntry }) { const actions = [ ...(app.liveUrl ? [{ label: appsCopy.liveAppLabel, href: app.liveUrl }] : []), ...(app.archiveUrl ? [{ label: appsCopy.archiveLabel, href: app.archiveUrl }] : []), + ...(app.brkUrl ? [{ label: appsCopy.exploreBrkLabel, href: app.brkUrl }] : []), { label: appsCopy.sourceCodeLabel, href: app.sourceUrl }, ]; diff --git a/src/content/apps.test.ts b/src/content/apps.test.ts index 9beb0b5..bf10d4b 100644 --- a/src/content/apps.test.ts +++ b/src/content/apps.test.ts @@ -3,12 +3,12 @@ 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" }, + { slug: "titanskies", title: "TitanSkies", status: undefined, liveUrl: "https://www.titanskies.com/", archiveUrl: undefined, brkUrl: undefined, sourceUrl: "https://github.com/hypertrial/titanskies" }, + { slug: "hyperoptions", title: "HyperOptions", status: undefined, liveUrl: undefined, archiveUrl: undefined, brkUrl: undefined, sourceUrl: "https://github.com/hypertrial/hyperoptions" }, + { slug: "travelcanary", title: "TravelCanary", status: "beta", liveUrl: "https://travelcanary.org/", archiveUrl: undefined, brkUrl: undefined, sourceUrl: "https://github.com/hypertrial/travelcanary" }, + { slug: "househunter", title: "HouseHunter", status: "alpha", liveUrl: undefined, archiveUrl: undefined, brkUrl: undefined, sourceUrl: "https://github.com/hypertrial/househunter" }, + { slug: "rockyroad", title: "RockyRoad", status: "alpha", liveUrl: undefined, archiveUrl: undefined, brkUrl: undefined, sourceUrl: "https://github.com/hypertrial/rockyroad" }, + { slug: "stackingsats", title: "StackingSats", status: "archived", liveUrl: undefined, archiveUrl: "https://stackingsats.org/", brkUrl: "https://bitview.space/", sourceUrl: "https://github.com/hypertrial/stacksats" }, ] as const; function expectHttps(raw: string) { @@ -21,8 +21,8 @@ function expectHttps(raw: string) { 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, + expect(apps.map(({ slug, title, status, liveUrl, archiveUrl, brkUrl, sourceUrl }) => ({ + slug, title, status, liveUrl, archiveUrl, brkUrl, sourceUrl, }))).toEqual(expectedApps); expect(new Set(apps.map(({ slug }) => slug)).size).toBe(6); expect(getAppBySlug("")).toBeUndefined(); @@ -45,7 +45,7 @@ describe("reviewed app catalog", () => { 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]) { + for (const url of [app.sourceUrl, app.sourceEvidenceUrl, app.liveUrl, app.archiveUrl, app.brkUrl]) { if (url) expectHttps(url); } expect(app.sourceEvidenceUrl, app.slug).toContain(app.sourceRevision); diff --git a/src/content/apps.ts b/src/content/apps.ts index cc8c03f..0e1fb80 100644 --- a/src/content/apps.ts +++ b/src/content/apps.ts @@ -23,6 +23,7 @@ export type AppEntry = { status?: "alpha" | "beta" | "archived"; liveUrl?: string; archiveUrl?: string; + brkUrl?: string; sourceUrl: string; sourceRevision: string; sourceEvidenceUrl: string; diff --git a/src/content/apps/other.ts b/src/content/apps/other.ts index d97be87..d748782 100644 --- a/src/content/apps/other.ts +++ b/src/content/apps/other.ts @@ -399,11 +399,12 @@ export const otherApps = [ summary: "Archived Bitcoin accumulation research site and Python library for testing fixed-budget allocation strategies.", status: "archived", archiveUrl: "https://stackingsats.org/", + brkUrl: "https://bitview.space/", 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.", + 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. Bitview is BRK's separate hosted explorer; it does not feed the archived site or library.", sources: [ { name: "Bitcoin Research Kit merged metrics", @@ -411,7 +412,10 @@ export const otherApps = [ 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" }], + additionalUrls: [ + { label: "Pinned parquet artifact", href: "https://drive.google.com/file/d/1jKRRU7l9kOMdGI_hIJGg02X3jWTMPJsw/view?usp=sharing" }, + { label: "Hosted BRK data (Bitview)", href: "https://bitview.space/" }, + ], 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.", diff --git a/src/content/site-copy.ts b/src/content/site-copy.ts index 0eca670..59ecc8a 100644 --- a/src/content/site-copy.ts +++ b/src/content/site-copy.ts @@ -57,6 +57,7 @@ export const appsCopy = { }, liveAppLabel: "Live App", archiveLabel: "Archive", + exploreBrkLabel: "Explore BRK Data", sourceCodeLabel: "Source Code", } as const;