diff --git a/apps/web/.env.example b/apps/web/.env.example index 71d59fb5..4fe9c2dd 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -6,7 +6,10 @@ VITE_API_BASE_URL=http://localhost:8080 # Example values: dealbot.filoz.org or staging.dealbot.filoz.org VITE_PLAUSIBLE_DATA_DOMAIN= -# Dashboard URL linked from the landing page +# Metrics dashboard URL linked from the per-SP rows on the landing page. +# One dashboard per environment; the app adds a vs[network] filter per row so it +# serves every network the deployment monitors (a no-op until the dashboard +# defines a {{network}} variable). # Example: https://your-team.betteruptime.com VITE_DASHBOARD_URL= @@ -14,12 +17,16 @@ VITE_DASHBOARD_URL= # Example: https://your-team.betteruptime.com/embed/... VITE_DASHBOARD_EMBED_URL= -# Combined approved-SP dashboard URL linked from the landing page, per network (optional) -# The frontend picks the URL matching the backend's network (mainnet vs calibration). -# Example mainnet: https://telemetry.betterstack.com/dashboards/Zz8k7L?rf=now-72h&rt=now -# Example calibration: https://telemetry.betterstack.com/dashboards/wt6QCw?rf=now-3d&rt=now +# Combined approved-SP dashboard URL linked from the landing page (optional). +# Preferred: one per-environment dashboard; the app adds a vs[network] filter. +# When set, it wins over the legacy per-network vars below. +# Example: https://telemetry.betterstack.com/dashboards/Zz8k7L?rf=now-72h&rt=now +VITE_APPROVED_SP_DASHBOARD_URL= +# Legacy per-network approved-SP dashboards (fallback used when the combined var +# above is unset). Each deployment sets the one matching its network. VITE_APPROVED_SP_DASHBOARD_URL_MAINNET= VITE_APPROVED_SP_DASHBOARD_URL_CALIBRATION= -# Logs URL linked from the landing page (optional) +# Logs URL linked from the per-SP rows on the landing page (optional). +# One dashboard per environment; the app adds a vs[network] filter per row. VITE_LOGS_URL= diff --git a/apps/web/docker-entrypoint.sh b/apps/web/docker-entrypoint.sh index 06b88235..893dd17f 100644 --- a/apps/web/docker-entrypoint.sh +++ b/apps/web/docker-entrypoint.sh @@ -11,6 +11,7 @@ API_BASE_URL="${DEALBOT_API_BASE_URL:-${VITE_API_BASE_URL:-}}" PLAUSIBLE_DATA_DOMAIN="${VITE_PLAUSIBLE_DATA_DOMAIN:-}" DASHBOARD_URL="${DASHBOARD_URL:-${VITE_DASHBOARD_URL:-}}" DASHBOARD_EMBED_URL="${DASHBOARD_EMBED_URL:-${VITE_DASHBOARD_EMBED_URL:-}}" +APPROVED_SP_DASHBOARD_URL="${APPROVED_SP_DASHBOARD_URL:-${VITE_APPROVED_SP_DASHBOARD_URL:-}}" APPROVED_SP_DASHBOARD_URL_MAINNET="${APPROVED_SP_DASHBOARD_URL_MAINNET:-${VITE_APPROVED_SP_DASHBOARD_URL_MAINNET:-}}" APPROVED_SP_DASHBOARD_URL_CALIBRATION="${APPROVED_SP_DASHBOARD_URL_CALIBRATION:-${VITE_APPROVED_SP_DASHBOARD_URL_CALIBRATION:-}}" LOGS_URL="${LOGS_URL:-${VITE_LOGS_URL:-}}" @@ -19,6 +20,7 @@ API_BASE_URL_ESCAPED="$(escape_js "$API_BASE_URL")" PLAUSIBLE_DATA_DOMAIN_ESCAPED="$(escape_js "$PLAUSIBLE_DATA_DOMAIN")" DASHBOARD_URL_ESCAPED="$(escape_js "$DASHBOARD_URL")" DASHBOARD_EMBED_URL_ESCAPED="$(escape_js "$DASHBOARD_EMBED_URL")" +APPROVED_SP_DASHBOARD_URL_ESCAPED="$(escape_js "$APPROVED_SP_DASHBOARD_URL")" APPROVED_SP_DASHBOARD_URL_MAINNET_ESCAPED="$(escape_js "$APPROVED_SP_DASHBOARD_URL_MAINNET")" APPROVED_SP_DASHBOARD_URL_CALIBRATION_ESCAPED="$(escape_js "$APPROVED_SP_DASHBOARD_URL_CALIBRATION")" LOGS_URL_ESCAPED="$(escape_js "$LOGS_URL")" @@ -29,6 +31,7 @@ window.__DEALBOT_CONFIG__ = { PLAUSIBLE_DATA_DOMAIN: "${PLAUSIBLE_DATA_DOMAIN_ESCAPED}", DASHBOARD_URL: "${DASHBOARD_URL_ESCAPED}", DASHBOARD_EMBED_URL: "${DASHBOARD_EMBED_URL_ESCAPED}", + APPROVED_SP_DASHBOARD_URL: "${APPROVED_SP_DASHBOARD_URL_ESCAPED}", APPROVED_SP_DASHBOARD_URL_MAINNET: "${APPROVED_SP_DASHBOARD_URL_MAINNET_ESCAPED}", APPROVED_SP_DASHBOARD_URL_CALIBRATION: "${APPROVED_SP_DASHBOARD_URL_CALIBRATION_ESCAPED}", LOGS_URL: "${LOGS_URL_ESCAPED}" diff --git a/apps/web/src/pages/Landing.test.tsx b/apps/web/src/pages/Landing.test.tsx index ec746f14..6d5d63aa 100644 --- a/apps/web/src/pages/Landing.test.tsx +++ b/apps/web/src/pages/Landing.test.tsx @@ -78,6 +78,9 @@ function renderLanding() { } describe("Landing", () => { + afterEach(() => { + window.__DEALBOT_CONFIG__ = undefined; + }); beforeEach(() => { mockUseActiveNetworks.mockReturnValue({ activeNetworks: ["mainnet"], loading: false, error: null }); mockUseSelectedNetwork.mockReturnValue(["mainnet", vi.fn()]); @@ -98,6 +101,42 @@ describe("Landing", () => { expect(cells[1].textContent).toBe("—"); }); + it("scopes per-SP metrics and logs links with vs[network]", () => { + window.__DEALBOT_CONFIG__ = { + DASHBOARD_URL: "https://telemetry.betterstack.com/dashboards/metrics", + LOGS_URL: "https://telemetry.betterstack.com/dashboards/logs", + }; + setupMock([makeProvider({ providerId: "42" })]); + renderLanding(); + const metrics = screen.getByRole("link", { name: /Metrics/ }); + const logs = screen.getByRole("link", { name: /Logs/ }); + expect(metrics.getAttribute("href")).toContain("vs%5Bnetwork%5D=mainnet"); + expect(logs.getAttribute("href")).toContain("vs%5Bnetwork%5D=mainnet"); + }); + + it("prefers the combined approved-SP dashboard and scopes it with vs[network]", () => { + window.__DEALBOT_CONFIG__ = { + APPROVED_SP_DASHBOARD_URL: "https://telemetry.betterstack.com/dashboards/combined", + APPROVED_SP_DASHBOARD_URL_MAINNET: "https://telemetry.betterstack.com/dashboards/legacy-mainnet", + }; + setupMock(); + renderLanding(); + const href = screen.getByRole("link", { name: /View dashboard/ }).getAttribute("href") ?? ""; + expect(href).toContain("/dashboards/combined"); + expect(href).toContain("vs%5Bnetwork%5D=mainnet"); + }); + + it("falls back to the legacy per-network approved-SP dashboard when the combined var is unset", () => { + window.__DEALBOT_CONFIG__ = { + APPROVED_SP_DASHBOARD_URL_MAINNET: "https://telemetry.betterstack.com/dashboards/legacy-mainnet", + }; + setupMock(); + renderLanding(); + const href = screen.getByRole("link", { name: /View dashboard/ }).getAttribute("href") ?? ""; + expect(href).toContain("/dashboards/legacy-mainnet"); + expect(href).not.toContain("vs%5Bnetwork%5D"); + }); + it("shows config errors instead of leaving providers loading", () => { mockUseActiveNetworks.mockReturnValue({ activeNetworks: [], diff --git a/apps/web/src/pages/Landing.tsx b/apps/web/src/pages/Landing.tsx index 8af2d0e2..5ba4e49a 100644 --- a/apps/web/src/pages/Landing.tsx +++ b/apps/web/src/pages/Landing.tsx @@ -10,8 +10,9 @@ import type { Network } from "@/types/config"; /** * Builds a BetterStack dashboard or logs URL scoped to a single provider. - * Appends time range (now-24h to now) and provider filter. - * Metrics dashboard uses vs[provider_id]; logs dashboard uses vs[providerId]. + * Appends time range and provider filter. Metrics dashboard uses vs[provider_id]; + * logs dashboard uses vs[providerId]. Network scoping is layered on separately + * via withNetworkParam. */ function buildBetterStackUrlWithProvider( baseUrl: string, @@ -51,11 +52,42 @@ const getConfigUrl = (runtimeValue?: string, buildValue?: string) => { return { safe, isInvalid: Boolean(raw) && !safe }; }; +type NetworkUrlSource = { runtime?: string; build?: string }; + +/** + * Adds the vs[network] dashboard filter to a URL, preserving existing params. + * Empty/invalid input yields "", and a null network returns the URL unchanged. + * A no-op until the BetterStack dashboard defines a {{network}} variable, so it + * is safe to emit ahead of that dashboard change. + */ +const withNetworkParam = (baseUrl: string, network: Network | null): string => { + if (!baseUrl || !network) return baseUrl; + try { + const url = new URL(baseUrl); + url.searchParams.set("vs[network]", network); + return url.toString(); + } catch { + return ""; + } +}; + const getConfig = (network: Network | null) => { const runtimeConfig = typeof window === "undefined" ? undefined : window.__DEALBOT_CONFIG__; + // Metrics dashboard and SP logs are a single per-environment dashboard; the + // per-SP rows add a vs[network] filter (see withNetworkParam) so one dashboard + // serves every network the deployment monitors. const dashboardUrl = getConfigUrl(runtimeConfig?.DASHBOARD_URL, import.meta.env.VITE_DASHBOARD_URL); - const approvedSpSources: Record = { + const logsUrl = getConfigUrl(runtimeConfig?.LOGS_URL, import.meta.env.VITE_LOGS_URL); + + // Approved-SP dashboard: prefer the combined per-environment dashboard scoped + // with vs[network]; fall back to the legacy per-network dashboards so existing + // deployments keep working until infra sets APPROVED_SP_DASHBOARD_URL. + const approvedSpGlobal = getConfigUrl( + runtimeConfig?.APPROVED_SP_DASHBOARD_URL, + import.meta.env.VITE_APPROVED_SP_DASHBOARD_URL, + ); + const approvedSpPerNetworkSources: Record = { mainnet: { runtime: runtimeConfig?.APPROVED_SP_DASHBOARD_URL_MAINNET, build: import.meta.env.VITE_APPROVED_SP_DASHBOARD_URL_MAINNET, @@ -65,10 +97,15 @@ const getConfig = (network: Network | null) => { build: import.meta.env.VITE_APPROVED_SP_DASHBOARD_URL_CALIBRATION, }, }; - const approvedSpDashboardUrl = network - ? getConfigUrl(approvedSpSources[network].runtime, approvedSpSources[network].build) + const approvedSpPerNetwork = network + ? getConfigUrl(approvedSpPerNetworkSources[network].runtime, approvedSpPerNetworkSources[network].build) : { safe: "", isInvalid: false }; - const logsUrl = getConfigUrl(runtimeConfig?.LOGS_URL, import.meta.env.VITE_LOGS_URL); + let approvedSpDashboardUrl = approvedSpPerNetwork; + if (approvedSpGlobal.safe) { + approvedSpDashboardUrl = { safe: withNetworkParam(approvedSpGlobal.safe, network), isInvalid: false }; + } else if (approvedSpGlobal.isInvalid) { + approvedSpDashboardUrl = approvedSpGlobal; + } return { dashboardUrl: dashboardUrl.safe, @@ -142,9 +179,9 @@ export default function Landing() {

{approvedSpDashboardUrlInvalid && selectedNetwork && (

- Warning: APPROVED_SP_DASHBOARD_URL_{selectedNetwork.toUpperCase()} (or{" "} - VITE_APPROVED_SP_DASHBOARD_URL_{selectedNetwork.toUpperCase()}) configured but invalid. Link - unavailable. + Warning: approved-SP dashboard URL (APPROVED_SP_DASHBOARD_URL or{" "} + APPROVED_SP_DASHBOARD_URL_{selectedNetwork.toUpperCase()}, or their VITE_-prefixed + build-time equivalents) configured but invalid. Link unavailable.

)}

@@ -247,11 +284,17 @@ export default function Landing() { const providerId = provider.providerId; const metricsHref = dashboardUrl && providerId != null - ? buildBetterStackUrlWithProvider(dashboardUrl, providerId, "vs[provider_id]") + ? withNetworkParam( + buildBetterStackUrlWithProvider(dashboardUrl, providerId, "vs[provider_id]"), + selectedNetwork, + ) : ""; const logsHref = logsUrl && providerId != null - ? buildBetterStackUrlWithProvider(logsUrl, providerId, "vs[providerId]") + ? withNetworkParam( + buildBetterStackUrlWithProvider(logsUrl, providerId, "vs[providerId]"), + selectedNetwork, + ) : ""; return ( diff --git a/apps/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts index e2df724c..9eda4711 100644 --- a/apps/web/src/vite-env.d.ts +++ b/apps/web/src/vite-env.d.ts @@ -8,6 +8,7 @@ interface ImportMeta { readonly VITE_PLAUSIBLE_DATA_DOMAIN?: string; readonly VITE_DASHBOARD_URL?: string; readonly VITE_DASHBOARD_EMBED_URL?: string; + readonly VITE_APPROVED_SP_DASHBOARD_URL?: string; readonly VITE_APPROVED_SP_DASHBOARD_URL_MAINNET?: string; readonly VITE_APPROVED_SP_DASHBOARD_URL_CALIBRATION?: string; readonly VITE_LOGS_URL?: string; @@ -20,6 +21,7 @@ interface Window { PLAUSIBLE_DATA_DOMAIN?: string; DASHBOARD_URL?: string; DASHBOARD_EMBED_URL?: string; + APPROVED_SP_DASHBOARD_URL?: string; APPROVED_SP_DASHBOARD_URL_MAINNET?: string; APPROVED_SP_DASHBOARD_URL_CALIBRATION?: string; LOGS_URL?: string;