mirror of
https://github.com/dcarrillo/atalaya.git
synced 2026-04-18 02:24:05 +00:00
feat: add custom banner to status page (#4)
This commit is contained in:
16
AGENTS.md
16
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
|
||||
|
||||
14
README.md
14
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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
<header class="header">
|
||||
<div class="header-top">
|
||||
{banner?.url ? (
|
||||
<div class="banner-container">
|
||||
{banner.link ? (
|
||||
<a href={banner.link} class="banner-link" aria-label={title} target="_self">
|
||||
<img src={banner.url} alt={title} class="banner-image" />
|
||||
</a>
|
||||
) : (
|
||||
<img src={banner.url} alt={title} class="banner-image" />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<h1>{title}</h1>
|
||||
)}
|
||||
<button class="theme-toggle" aria-label="Toggle theme" title="Toggle light/dark theme">
|
||||
<svg class="theme-icon sun" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="5"/>
|
||||
@@ -156,6 +172,29 @@ const absoluteTime = formatAbsoluteTime(lastUpdated);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.banner-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.banner-link {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
transition: opacity var(--transition-normal);
|
||||
}
|
||||
|
||||
.banner-link:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.banner-image {
|
||||
max-height: 60px;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.theme-icon {
|
||||
transition: opacity var(--transition-normal), transform var(--transition-normal);
|
||||
}
|
||||
@@ -275,6 +314,10 @@ const absoluteTime = formatAbsoluteTime(lastUpdated);
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
|
||||
.banner-image {
|
||||
max-height: 50px;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
@@ -302,6 +345,10 @@ const absoluteTime = formatAbsoluteTime(lastUpdated);
|
||||
font-size: var(--text-xl);
|
||||
}
|
||||
|
||||
.banner-image {
|
||||
max-height: 40px;
|
||||
}
|
||||
|
||||
.summary-counts {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { env } from 'cloudflare:workers';
|
||||
|
||||
let data: Awaited<ReturnType<typeof getStatusApiData>> | null = null;
|
||||
let error: Error | null = null;
|
||||
let banner: { url: string; link?: string } | undefined = undefined;
|
||||
|
||||
try {
|
||||
// TypeScript doesn't know about MONITORS_CONFIG in cloudflare:workers env
|
||||
@@ -21,6 +22,10 @@ try {
|
||||
const configYaml = interpolateSecrets(monitorsConfig, envAny);
|
||||
const config = parseConfig(configYaml);
|
||||
data = await getStatusApiData(env.DB, config);
|
||||
|
||||
const bannerUrl = envAny.STATUS_BANNER_URL;
|
||||
const bannerLink = envAny.STATUS_BANNER_LINK;
|
||||
banner = bannerUrl ? { url: bannerUrl, link: bannerLink } : undefined;
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch status data:', err);
|
||||
error = err as Error;
|
||||
@@ -55,7 +60,7 @@ const sortedMonitors = data ? [...data.monitors].sort(
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Header summary={data!.summary} lastUpdated={data!.lastUpdated} title={data!.title} />
|
||||
<Header summary={data!.summary} lastUpdated={data!.lastUpdated} title={data!.title} banner={banner} />
|
||||
<div class="monitors">
|
||||
{sortedMonitors.map(monitor => <MonitorCard monitor={monitor} />)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user