feat(web): admin area with instance default theme, custom logo/favicon; .theme.json export
- Admin settings tab (admin-only) now holds user management plus a server-wide default theme editor (live preview + discard) and logo/favicon branding. - New singleton InstanceSettings model + /api/instance router (public GET for the login screen; admin-gated theme PUT and logo/favicon upload/delete, reusing the avatar PIL/validation pattern). - Instance default theme is the base users inherit and the target a per-colour "Reset" returns to (baseColor: instance default -> built-in). - Custom favicon overrides the primary-colour tinting; custom logo replaces the top-left glyph+text, hard-capped so it only scales down and never breaks layout. Branding + defaults load before login (public endpoint). - Theme export now uses the recognised .theme.json extension (old .theme still imports); import stays partial/unknown-key aware. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
56
frontend/js/instance.js
Normal file
56
frontend/js/instance.js
Normal file
@@ -0,0 +1,56 @@
|
||||
// Instance-wide branding + default theme (public GET /api/instance/). Loaded as
|
||||
// early as possible so the login screen is already branded. See
|
||||
// backend/routers/admin_router.py.
|
||||
|
||||
import { setInstanceDefaults } from './settings-sync.js';
|
||||
import { setCustomFavicon, applyFavicon } from './utils.js';
|
||||
|
||||
export let instanceConfig = {};
|
||||
|
||||
// Cache the bundled default logo markup so "remove logo" can restore it.
|
||||
const originalLogoHtml = {};
|
||||
|
||||
function setBrandLogo(url) {
|
||||
document.querySelectorAll('.topbar-logo, .auth-logo').forEach(el => {
|
||||
const key = el.classList.contains('topbar-logo') ? 'topbar' : 'auth';
|
||||
if (!(key in originalLogoHtml)) originalLogoHtml[key] = el.innerHTML;
|
||||
if (url) {
|
||||
const cls = key === 'topbar' ? 'topbar-logo-img' : 'auth-logo-img';
|
||||
el.innerHTML = `<img class="${cls}" src="${url}" alt="Logo">`;
|
||||
} else {
|
||||
el.innerHTML = originalLogoHtml[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function applyInstanceBranding(cfg) {
|
||||
cfg = cfg || {};
|
||||
setInstanceDefaults(cfg.default_theme || {});
|
||||
setCustomFavicon(cfg.has_favicon ? cfg.favicon_url : null);
|
||||
setBrandLogo(cfg.has_logo ? cfg.logo_url : null);
|
||||
applyFavicon(); // no arg → uses instance/built-in primary as fallback
|
||||
}
|
||||
|
||||
// Memoised so boot() and initCalendar() share a single fetch; callers can await
|
||||
// it to guarantee instance defaults are set before the first applyTheme().
|
||||
let loadPromise = null;
|
||||
export function loadInstance() {
|
||||
if (loadPromise) return loadPromise;
|
||||
loadPromise = (async () => {
|
||||
try {
|
||||
const res = await fetch('/api/instance/', { headers: { Accept: 'application/json' } });
|
||||
instanceConfig = res.ok ? await res.json() : {};
|
||||
} catch (_) {
|
||||
instanceConfig = {};
|
||||
}
|
||||
applyInstanceBranding(instanceConfig);
|
||||
return instanceConfig;
|
||||
})();
|
||||
return loadPromise;
|
||||
}
|
||||
|
||||
// Force a re-fetch (after an admin changes branding/theme).
|
||||
export function reloadInstance() {
|
||||
loadPromise = null;
|
||||
return loadInstance();
|
||||
}
|
||||
Reference in New Issue
Block a user