Files
Calendarr/frontend/js/instance.js
Scarriffle cea96660d9 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>
2026-07-20 14:19:15 +02:00

57 lines
1.9 KiB
JavaScript

// 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();
}