feat(banners): instance announcements — admin CRUD, top bar, CLI (#39)

banners table (title/body/level/active/dismissible/schedule window).
GET /banners/active for signed-in users; admin CRUD under /admin/banners.
BannerBar renders above the header with per-banner dismissal
(localStorage); admin console gains an Announcements section;
`containr banners` manages them and `auth status` prints active ones.

Generated with [Devin](https://devin.ai)

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomáš DvořákandDevin authored and GitHub committed 2026-10-09 10:10:18 +02:00
1 parent 954a1d1f80
commit 9408464f12
21 files changed
+1301 -3

No files matched your search

+44
View File
@@ -1866,6 +1866,50 @@ export async function getOperations(): Promise<OperationsView> {
return requestJson<OperationsView>('/operations');
}
export type Banner = {
id: string;
title: string;
body: string;
level: 'info' | 'success' | 'warning' | 'error';
active: boolean;
dismissible: boolean;
starts_at?: string | null;
ends_at?: string | null;
created_at: string;
};
export type BannerInput = {
title?: string;
body?: string;
level?: Banner['level'];
active?: boolean;
dismissible?: boolean;
starts_at?: string;
ends_at?: string;
};
export async function listActiveBanners(): Promise<Banner[]> {
const payload = await requestJson<{ banners?: Banner[] }>('/banners/active');
return payload.banners ?? [];
}
export async function listAdminBanners(): Promise<Banner[]> {
const payload = await requestJson<{ banners?: Banner[] }>('/admin/banners');
return payload.banners ?? [];
}
export async function createBanner(input: BannerInput): Promise<Banner> {
return requestJson<Banner>('/admin/banners', { method: 'POST', body: JSON.stringify(input) });
}
export async function updateBanner(id: string, input: BannerInput): Promise<Banner> {
return requestJson<Banner>(`/admin/banners/${id}`, { method: 'PATCH', body: JSON.stringify(input) });
}
export async function deleteBanner(id: string): Promise<void> {
await requestJson(`/admin/banners/${id}`, { method: 'DELETE' });
}
export function serviceStatusClass(status: ServiceStatus): string {
switch (status) {
case 'running':