Skip to content

feat(create-a-container): admin-configurable announcement banner#424

Open
runleveldev wants to merge 1 commit into
mieweb:mainfrom
runleveldev:announcement-banner
Open

feat(create-a-container): admin-configurable announcement banner#424
runleveldev wants to merge 1 commit into
mieweb:mainfrom
runleveldev:announcement-banner

Conversation

@runleveldev

Copy link
Copy Markdown
Collaborator

Summary

Adds a deployment-specific announcement banner shown to all users at the top of the app. The message is stored in the existing admin-editable Settings table, so each environment controls its own banner at runtime — no hardcoded URLs in the repo, no env vars, no restart.

Motivating use case: linking users to a companion app whose URL varies per environment, e.g. Try [Ozwell Studio](https://ozwell-studio-launcher.os.mieweb.org).

How it works

Settings (admin sidebar) → Announcement banner → save. The banner renders immediately for all users; clearing the field hides it.

  • New banner_message key in the settings whitelist (PUT/GET /api/v1/settings, stored trimmed)
  • Exposed to everyone via the unauthenticated GET /api/v1/health as banner, mirroring the existing isDev/oidcEnabled flow into useServerInfo(); wrapped in try/catch so a DB hiccup never fails the health check
  • New AppBanner component mounted in AppLayout between the header and main content
  • Supports [text](url) link syntax, parsed client-side into <a> elements (http/https only, no HTML injection) so admin-provided content stays XSS-safe
  • Saving settings invalidates the useServerInfo cache so admins see the change without a reload
  • Admin docs updated (mie-opensource-landing/docs/admins/settings.md)

Testing

  • New integration suite routers/api/v1/__tests__/banner.api.test.js: default-null banner, admin write + unauthenticated exposure via health, non-admin 403, clearing hides it
  • Full server suite passes: 4 suites / 28 tests (24 pre-existing + 4 new)
  • Client tsc -b clean; production build succeeds

Notes

  • useServerInfo caches with staleTime: Infinity, so already-active users pick up banner changes on their next page load
  • The banner currently renders only in the authenticated app shell; it's a one-line addition to AuthLayout if we also want it on the login page

Adds a banner_message setting (admin Settings page) exposed through the
unauthenticated /api/v1/health endpoint and rendered as a strip at the
top of the app shell. Supports [text](url) links, parsed client-side
into anchors (http/https only) so admin-provided content stays XSS-safe.

Lets each deployment announce environment-specific links (e.g. a local
Ozwell Studio launcher) at runtime, without hardcoding URLs in the repo
or requiring a restart.
Copilot AI review requested due to automatic review settings July 23, 2026 19:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an admin-configurable announcement banner to the create-a-container SPA, stored per-deployment in the existing Settings table and exposed to clients via the unauthenticated /api/v1/health server-info endpoint.

Changes:

  • Add banner_message to admin settings GET/PUT, trimming on write.
  • Extend /api/v1/health to include banner and add client-side useServerInfo() consumption + cache invalidation on settings save.
  • Add a new AppBanner UI component (with [text](url) link parsing) mounted in AppLayout, plus integration tests and admin docs.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
mie-opensource-landing/docs/admins/settings.md Documents the new Announcement Banner setting for admins.
create-a-container/routers/api/v1/settings.js Adds banner_message to the admin settings API payload and persists it trimmed.
create-a-container/routers/api/v1/index.js Exposes banner via unauthenticated /health so the SPA can render it.
create-a-container/routers/api/v1/tests/banner.api.test.js Integration coverage for default banner state, admin write, non-admin denial, and health exposure.
create-a-container/client/src/pages/settings/SettingsPage.tsx Adds banner field to the settings form and invalidates cached server-info on save.
create-a-container/client/src/lib/types.ts Extends AppSettings with bannerMessage.
create-a-container/client/src/lib/auth.ts Extends ServerInfo with banner and exports serverInfoKey used for invalidation.
create-a-container/client/src/app/Banner.tsx New banner component + link parser for [text](url) syntax.
create-a-container/client/src/app/AppLayout.tsx Mounts the banner between the header and main content.

Comment on lines +14 to +28
for (const match of message.matchAll(linkPattern)) {
if (match.index > cursor) nodes.push(message.slice(cursor, match.index));
nodes.push(
<a
key={match.index}
href={match[2]}
target="_blank"
rel="noopener noreferrer"
className="font-semibold underline underline-offset-2 hover:opacity-80"
>
{match[1]}
</a>,
);
cursor = match.index + match[0].length;
}
Comment on lines +43 to +47
router.get('/health', async (_req, res) => {
// The banner is cosmetic — never let a DB hiccup fail the health check.
let banner = null;
try {
banner = (await Setting.get('banner_message'))?.trim() || null;
Comment on lines +44 to +48
// The banner is cosmetic — never let a DB hiccup fail the health check.
let banner = null;
try {
banner = (await Setting.get('banner_message'))?.trim() || null;
} catch {

Display a one-line announcement to all users at the top of the app — useful for deployment-specific notices like linking to a companion app hosted in the same environment.

- **Banner message**: plain text, with optional links using `[text](url)` syntax, e.g. `Try [Ozwell Studio](https://ozwell-studio.example.org).`
<Input
label="Banner message"
placeholder="Try [My New App](https://app.example.com)."
helperText="Shown to all users at the top of the app. Link syntax: [text](url). Leave empty to hide the banner."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants