Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .build/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ ENV VITE_DISABLED_TABS=__VITE_DISABLED_TABS__
ENV VITE_ENABLED_THEMES=__VITE_ENABLED_THEMES__
ENV VITE_APP_NAME=__VITE_APP_NAME__
ENV VITE_SKIP_SPLASH=__VITE_SKIP_SPLASH__
ENV VITE_BANNER=__VITE_BANNER__
RUN npm run build

FROM caddy:2-alpine
Expand Down
3 changes: 3 additions & 0 deletions .build/docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ ENABLED_THEMES="${VITE_ENABLED_THEMES:-}"
SKIP_SPLASH="${VITE_SKIP_SPLASH:-}"
# APP_NAME is substituted verbatim; avoid '&' and '|' (sed replacement metachar and delimiter).
APP_NAME="${VITE_APP_NAME:-BEACON}"
# Same caveat as APP_NAME, plus no quotes (it lands inside a JS string).
BANNER="${VITE_BANNER:-}"

find /srv -name '*.js' -exec sed -i \
-e "s|__VITE_API_BASE__|${API_BASE}|g" \
Expand All @@ -22,6 +24,7 @@ find /srv -name '*.js' -exec sed -i \
-e "s|__VITE_ENABLED_THEMES__|${ENABLED_THEMES}|g" \
-e "s|__VITE_APP_NAME__|${APP_NAME}|g" \
-e "s|__VITE_SKIP_SPLASH__|${SKIP_SPLASH}|g" \
-e "s|__VITE_BANNER__|${BANNER}|g" \
{} +

exec "$@"
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@

# Skip the once-per-session load splash. Unset = shown.
# VITE_SKIP_SPLASH=true

# Notice shown above the header on every page; [label](url) and bare URLs become links. Unset = none. Avoid &, | and quotes.
# VITE_BANNER=This is the Beacon development instance and may be unstable. For everyday use, visit [live.meshcore.ca](https://live.meshcore.ca).
3 changes: 3 additions & 0 deletions docker/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ VITE_APP_NAME=

# Skip the once-per-session load splash. Set to true to skip; leave empty to show it.
VITE_SKIP_SPLASH=

# Notice above the header (e.g. on a dev instance); [label](url) and bare URLs become links. Blank = none.
VITE_BANNER=
2 changes: 2 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ services:
- VITE_APP_NAME=${VITE_APP_NAME:-BEACON}
# Skip the once-per-session load splash; unset = shown
- VITE_SKIP_SPLASH=${VITE_SKIP_SPLASH:-}
# Notice above the header, e.g. on a dev instance; unset = none
- VITE_BANNER=${VITE_BANNER:-}
restart: unless-stopped
4 changes: 3 additions & 1 deletion src/components/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { LanguageOptions, LanguagePicker } from "./LanguagePicker";
import { useIsMobile } from "../hooks/useMediaQuery";
import { BeaconWordmark } from "./BeaconWordmark";
import { getIatas } from "../api/client";
import { ENABLED_TABS, ENABLED_THEME_IDS, selectableThemes, APP_NAME, GITHUB_URL } from "../lib/constants";
import { ENABLED_TABS, ENABLED_THEME_IDS, selectableThemes, APP_NAME, GITHUB_URL, BANNER } from "../lib/constants";
import { InstanceBanner } from "./InstanceBanner";
import type { WsManager } from "../api/ws-manager";

// header widgets: WS status, region picker, theme picker
Expand Down Expand Up @@ -430,6 +431,7 @@ export function AppShell({ activeTab, onTabChange, wsManager, children }: AppShe
const isMobile = useIsMobile();
return (
<div className="flex flex-col h-dvh">
<InstanceBanner text={BANNER} />
{isMobile ? (
<header className="flex items-center gap-2 px-3 py-1.5 min-h-[42px] bg-bg-surface border-b border-border shrink-0">
<BeaconWordmark iconSize={20} textClassName="text-sm" className="shrink-0" />
Expand Down
22 changes: 22 additions & 0 deletions src/components/InstanceBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// [label](url) or a bare URL; trailing punctuation and closing parens stay outside a bare link
const LINK_PATTERN = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)|(https?:\/\/[^\s()]*[^\s().,;:!?])/g;

// Deployment-wide notice set by VITE_BANNER, e.g. flagging the dev instance.
export function InstanceBanner({ text }: { text: string }) {
const trimmed = text.trim();
if (!trimmed) return null;
const parts = [];
let last = 0;
for (const m of trimmed.matchAll(LINK_PATTERN)) {
parts.push(trimmed.slice(last, m.index));
const href = m[2] ?? m[3];
parts.push(<a key={m.index} href={href} className="underline underline-offset-2 hover:text-text-bright">{m[1] ?? href}</a>);
last = m.index + m[0].length;
}
parts.push(trimmed.slice(last));
return (
<div role="note" className="shrink-0 border-b border-warn/30 bg-warn/10 px-4 py-1.5 text-center font-mono text-[12px] text-warn">
{parts}
</div>
);
}
3 changes: 3 additions & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,8 @@ export const ENABLED_THEME_IDS = new Set(
export const APP_NAME = import.meta.env.VITE_APP_NAME || "BEACON";
export const GITHUB_URL = "https://github.com/MeshCore-Beacon";

// Notice shown above the header on every page; unset = no banner.
export const BANNER = import.meta.env.VITE_BANNER ?? "";

// Skip the once-per-session load splash entirely (e.g. an embedded/branded deployment).
export const SKIP_SPLASH = parseEnvBool(import.meta.env.VITE_SKIP_SPLASH);
22 changes: 22 additions & 0 deletions tests/components/InstanceBanner.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { InstanceBanner } from "../../src/components/InstanceBanner";

describe("InstanceBanner", () => {
it("renders nothing without text", () => {
const { container } = render(<InstanceBanner text=" " />);
expect(container).toBeEmptyDOMElement();
});
it("shows the text and links any URL in it", () => {
render(<InstanceBanner text="This is the development instance of Beacon, please use live (https://live.meshcore.ca)" />);
const banner = screen.getByRole("note");
expect(banner).toHaveTextContent("This is the development instance of Beacon, please use live (https://live.meshcore.ca)");
const link = screen.getByRole("link", { name: "https://live.meshcore.ca" });
expect(link).toHaveAttribute("href", "https://live.meshcore.ca");
});
it("turns [label](url) into a labelled link", () => {
render(<InstanceBanner text="This is the Beacon development instance and may be unstable. For everyday use, visit [live.meshcore.ca](https://live.meshcore.ca)." />);
expect(screen.getByRole("note")).toHaveTextContent("This is the Beacon development instance and may be unstable. For everyday use, visit live.meshcore.ca.");
expect(screen.getByRole("link", { name: "live.meshcore.ca" })).toHaveAttribute("href", "https://live.meshcore.ca");
});
});
Loading