From 2a4bf3b868c6733d0b865c8c0e977ba84d6e6453 Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Mon, 29 Jun 2026 22:19:22 +0200 Subject: [PATCH] feat(admin): in-app migration banner for the org rename (#669) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One-time banner shown at the top of the admin layout to surface the org rename + GHCR registry change for operators who haven't read the release notes. Sits right below the existing maintenance banner — same pattern. ## What it looks like Blue, dismissible banner with a short body: > PicPeak's image registry has moved > Update your docker-compose.yml to pull from > ghcr.io/picpeak/picpeak/{backend,frontend} — the old path is no longer > being updated. [See migration notes] The link goes to `docs/migration-to-org.md` on the new org repo. ## Design choices - **No backend feature flag.** A hard-coded `MIGRATION_BANNER_ENABLED` constant in the component file (1 line) gates global display. After ~1 quarter, flip it to false (or drop the mount in `AdminLayout.tsx`) in a small follow-up PR. A backend `app_settings` row + Settings UI toggle would be overkill for a one-time migration event. - **Per-admin dismissal via localStorage.** Key is `picpeak:migration-banner:v1` (versioned so a future "we've moved AGAIN" banner can show without inheriting earlier dismissal). Wrapped in try/catch so private-mode browsers + storage-quota-exceeded errors don't crash the layout. - **EN + DE strings** under a new top-level `migrationBanner` namespace. Other locales (fr, nl, pt, ru) fall through to EN — `migrationBanner.*` keys aren't translated there yet, deliberate (per #669 the ops banner is operator-facing and admins reading EN/DE is the majority). - **Reuses `common.dismiss`** for the close-button aria-label. ## Test plan - [ ] Frontend `npm run build:check` passes (TS + build) - [ ] Open admin dashboard in EN → banner shows at top, below header, above main content - [ ] Switch to DE → banner shows German strings - [ ] Click dismiss → banner hides, doesn't re-appear on hard refresh - [ ] Clear localStorage `picpeak:migration-banner:v1` → banner returns - [ ] Flip `MIGRATION_BANNER_ENABLED` to false → banner doesn't render for anyone, regardless of dismissal state Refs #669. --- frontend/src/components/admin/AdminLayout.tsx | 6 ++ .../src/components/admin/MigrationBanner.tsx | 69 +++++++++++++++++++ frontend/src/i18n/locales/de.json | 5 ++ frontend/src/i18n/locales/en.json | 5 ++ 4 files changed, 85 insertions(+) create mode 100644 frontend/src/components/admin/MigrationBanner.tsx diff --git a/frontend/src/components/admin/AdminLayout.tsx b/frontend/src/components/admin/AdminLayout.tsx index 7eef8a12..f78ea4aa 100644 --- a/frontend/src/components/admin/AdminLayout.tsx +++ b/frontend/src/components/admin/AdminLayout.tsx @@ -7,6 +7,7 @@ import { useSessionTimeout } from '../../hooks/useSessionTimeout'; import { AdminSidebar } from './AdminSidebar'; import { AdminHeader } from './AdminHeader'; import { MaintenanceBanner } from './MaintenanceBanner'; +import { MigrationBanner } from './MigrationBanner'; import { MandatoryPasswordChangeModal } from './MandatoryPasswordChangeModal'; const SIDEBAR_COLLAPSED_KEY = 'admin-sidebar-collapsed'; @@ -114,6 +115,11 @@ const AdminLayoutInner: React.FC = ({ sidebarOpen, setSid {/* Maintenance mode banner */} + {/* One-time migration banner — flip the constant in MigrationBanner.tsx + (or remove this mount) after operators have had time to update their + docker-compose.yml. See #669. */} + + {/* Page content - disabled when password change required. overflow moved up to the column so the scrollbar gutter is reserved once at the column level (see above). main now diff --git a/frontend/src/components/admin/MigrationBanner.tsx b/frontend/src/components/admin/MigrationBanner.tsx new file mode 100644 index 00000000..b48f65b8 --- /dev/null +++ b/frontend/src/components/admin/MigrationBanner.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { Info, X } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; + +// Hard-coded "feature flag" — flip to false (or remove this component from +// AdminLayout) once the migration window settles, after operators have had +// ~1 quarter to update their docker-compose.yml. See #669. +const MIGRATION_BANNER_ENABLED = true; + +// localStorage key — versioned (`:v1`) so a future "we've moved again" banner +// can show without inheriting the user's earlier dismissal. +const DISMISS_KEY = 'picpeak:migration-banner:v1'; + +/** + * One-time migration banner shown at the top of the admin layout (#669). + * + * Surfaces the org rename + new GHCR registry path so an operator who hasn't + * read the release notes sees the change when they next log in. Dismissible + * per-admin via localStorage; the toggle above can flip it off globally. + */ +export const MigrationBanner: React.FC = () => { + const { t } = useTranslation(); + const [dismissed, setDismissed] = React.useState(() => { + try { return localStorage.getItem(DISMISS_KEY) === '1'; } catch { return false; } + }); + + if (!MIGRATION_BANNER_ENABLED || dismissed) return null; + + const handleDismiss = () => { + try { localStorage.setItem(DISMISS_KEY, '1'); } catch { /* localStorage blocked */ } + setDismissed(true); + }; + + return ( +
+
+
+
+ +
+

{t('migrationBanner.title', "PicPeak's image registry has moved")}

+

+ {t('migrationBanner.body', { + defaultValue: 'Update your docker-compose.yml to pull from {{newPath}} — the old path is no longer being updated.', + newPath: 'ghcr.io/picpeak/picpeak/{backend,frontend}', + })}{' '} + + {t('migrationBanner.link', 'See migration notes')} + +

+
+
+ +
+
+
+ ); +}; diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 0a91dd84..b369a272 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -3065,6 +3065,11 @@ "message": "Wir führen derzeit geplante Wartungsarbeiten durch, um unseren Service zu verbessern. Wir sind in Kürze wieder online.", "urgentMatters": "Bei dringenden Anliegen kontaktieren Sie bitte" }, + "migrationBanner": { + "title": "Die Image-Registry von PicPeak wurde verschoben", + "body": "Aktualisiere deine docker-compose.yml, um Images von {{newPath}} zu beziehen — der alte Pfad wird nicht mehr aktualisiert.", + "link": "Migrationshinweise ansehen" + }, "passwordChange": { "title": "Passwort ändern", "currentPassword": "Aktuelles Passwort", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 95422e5c..a36136bf 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -3086,6 +3086,11 @@ "message": "We're currently performing scheduled maintenance to improve our service. We'll be back online shortly.", "urgentMatters": "For urgent matters, please contact" }, + "migrationBanner": { + "title": "PicPeak's image registry has moved", + "body": "Update your docker-compose.yml to pull from {{newPath}} — the old path is no longer being updated.", + "link": "See migration notes" + }, "passwordChange": { "title": "Change Password", "currentPassword": "Current Password",