diff --git a/AGENTS.md b/AGENTS.md index a8b9dc8..1e3326d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,7 @@ npm workspace: Cloudflare Worker (`src/`) + Astro 6 SSR app (`status-page/`). - **Static assets**: `status-page/dist/client/` served via `ASSETS` binding (`run_worker_first = true`) - **Shared types**: `src/types.ts` imported via `@worker/types` alias in `status-page/tsconfig.json` - **Auth**: `status-page/src/lib/auth.ts`, configured via `STATUS_PUBLIC`, `STATUS_USERNAME`, `STATUS_PASSWORD` env vars +- **Banner**: Custom banner image via `STATUS_BANNER_URL` and `STATUS_BANNER_LINK` env vars ## Commands @@ -58,3 +59,18 @@ Before commit: `npm run check && npm run test && npm run build:pages && npm run - **Types**: `type` for aliases, `interface` for object shapes. Explicit return types. - **Naming**: files `kebab-case.ts`, types `PascalCase`, variables `camelCase`, DB fields `snake_case`. - **Tests**: `*.test.ts` colocated with source. Vitest with `vi` for mocks. + +## Banner Configuration + +Set environment variables in `wrangler.toml` or Cloudflare dashboard: + +```toml +[vars] +STATUS_BANNER_URL = "https://example.com/banner.png" +STATUS_BANNER_LINK = "https://example.com" # optional +``` + +- Banner replaces title text when `STATUS_BANNER_URL` is set +- Title is used as `alt` text for accessibility +- Link opens in same tab when `STATUS_BANNER_LINK` is set +- Empty or unset URL falls back to title display diff --git a/README.md b/README.md index 8ec9542..3fc200c 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ __ ,--~' ~~----____ - Response time charts (uPlot) with downtime bands. - Basic auth or public access modes. - Dark/light mode. + - Custom banner image support with optional clickable link. ## Architecture @@ -135,7 +136,7 @@ settings: ### Per-Monitor Overrides -Each monitor can override the global default_* settings: +Each monitor can override the global default\_\* settings: ```yaml - name: 'critical-api' @@ -271,6 +272,17 @@ wrangler secret put STATUS_PUBLIC # Set value to "true" - If credentials are set: basic auth required - Otherwise: 403 Forbidden +**Custom Banner:** + +You can add a custom banner image to replace the title text on the status page: + +```toml +# In wrangler.toml +[vars] +STATUS_BANNER_URL = "https://example.com/banner.png" +STATUS_BANNER_LINK = "https://example.com" # optional +``` + ## Secret Management Secrets are managed via Cloudflare's secret system. To add a new secret: diff --git a/src/types.ts b/src/types.ts index 229216b..1113737 100644 --- a/src/types.ts +++ b/src/types.ts @@ -89,6 +89,8 @@ export type Env = { STATUS_PUBLIC?: string; REGIONAL_CHECKER_DO?: DurableObjectNamespace; ASSETS?: Fetcher; + STATUS_BANNER_URL?: string; + STATUS_BANNER_LINK?: string; }; // Status API response types (consumed by Pages project via service binding) diff --git a/status-page/src/components/Header.astro b/status-page/src/components/Header.astro index 5b53e8a..f193bd3 100644 --- a/status-page/src/components/Header.astro +++ b/status-page/src/components/Header.astro @@ -7,9 +7,13 @@ interface Props { }; lastUpdated: number; title: string; + banner?: { + url?: string; + link?: string; + }; } -const { summary, lastUpdated, title } = Astro.props; +const { summary, lastUpdated, title, banner } = Astro.props; function formatAbsoluteTime(unixTimestamp: number): string { const date = new Date(unixTimestamp * 1000); @@ -28,7 +32,19 @@ const absoluteTime = formatAbsoluteTime(lastUpdated);
-

{title}

+ {banner?.url ? ( + + ) : ( +

{title}

+ )}
) : ( <> -
+
{sortedMonitors.map(monitor => )}
@@ -80,7 +85,7 @@ const sortedMonitors = data ? [...data.monitors].sort( left: 0; width: 100%; height: 100%; - background-image: + background-image: linear-gradient(var(--border-subtle) 1px, transparent 1px), linear-gradient(90deg, var(--border-subtle) 1px, transparent 1px); background-size: 40px 40px; @@ -90,7 +95,7 @@ const sortedMonitors = data ? [...data.monitors].sort( } [data-theme='dark'] .container::before { - background-image: + background-image: linear-gradient(var(--border) 1px, transparent 1px), linear-gradient(90deg, var(--border) 1px, transparent 1px); opacity: 0.1; @@ -106,7 +111,7 @@ const sortedMonitors = data ? [...data.monitors].sort( .container { padding: var(--space-16) var(--space-12) var(--space-20); } - + .monitors { gap: var(--space-5); } @@ -201,18 +206,18 @@ const sortedMonitors = data ? [...data.monitors].sort( .container { padding: var(--space-5) var(--space-4); } - + .error-state, .loading-state, .empty-state { padding: var(--space-12) var(--space-4); } - + .error-icon, .empty-icon { font-size: 2.25rem; } - + .error-state h2, .empty-state h2 { font-size: var(--text-xl);