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
- 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 (