feat(create-a-container): admin-configurable announcement banner#424
Open
runleveldev wants to merge 1 commit into
Open
feat(create-a-container): admin-configurable announcement banner#424runleveldev wants to merge 1 commit into
runleveldev wants to merge 1 commit into
Conversation
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.
Contributor
There was a problem hiding this comment.
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_messageto admin settings GET/PUT, trimming on write. - Extend
/api/v1/healthto includebannerand add client-sideuseServerInfo()consumption + cache invalidation on settings save. - Add a new
AppBannerUI component (with[text](url)link parsing) mounted inAppLayout, 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." |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Settingstable, 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.
banner_messagekey in the settings whitelist (PUT/GET /api/v1/settings, stored trimmed)GET /api/v1/healthasbanner, mirroring the existingisDev/oidcEnabledflow intouseServerInfo(); wrapped in try/catch so a DB hiccup never fails the health checkAppBannercomponent mounted inAppLayoutbetween the header and main content[text](url)link syntax, parsed client-side into<a>elements (http/https only, no HTML injection) so admin-provided content stays XSS-safeuseServerInfocache so admins see the change without a reloadmie-opensource-landing/docs/admins/settings.md)Testing
routers/api/v1/__tests__/banner.api.test.js: default-null banner, admin write + unauthenticated exposure via health, non-admin 403, clearing hides ittsc -bclean; production build succeedsNotes
useServerInfocaches withstaleTime: Infinity, so already-active users pick up banner changes on their next page loadAuthLayoutif we also want it on the login page