feat(config + admin panel): handle features activated

- review admin dashboard to reflect features enabled/disabled
- hide mount option if feature is disabled
- remove QUOTA option as it is not wired
This commit is contained in:
Edouard Vanbelle
2026-09-11 13:02:33 +02:00
parent 5083eaeaba
commit 8d1fde2747
16 changed files with 303 additions and 134 deletions
+6 -2
View File
@@ -416,11 +416,15 @@ export interface AdminDashboard {
* `online_sessions / online_users` is the multi-device factor
* (browser + desktop + phone). */
online_sessions: number;
/** Currently-connected message-bus WebSocket sessions — one per
* open browser tab that reached a folder view. Reported as `0`
* when `OXICLOUD_MESSAGEBUS_ENABLE=false` (no live sessions
* possible); the dashboard hides the card in that case since
* the value would be misleading. */
active_ws_sessions: number;
server_version: string;
drive_usage: DriveKindUsage[];
auth_enabled: boolean;
oidc_configured: boolean;
quotas_enabled: boolean;
registration_enabled?: boolean;
users_over_80_percent: number;
users_over_quota: number;
+4 -1
View File
@@ -893,7 +893,10 @@ export interface ServerFeatures {
trash: boolean;
search: boolean;
sharing: boolean;
quotas: boolean;
// NOTE: `quotas` was intentionally NOT exposed — see the Rust
// `FeaturesDto` doc for why (dormant server flag with zero
// consumers). Add it back once it actually gates FE-visible
// behavior.
music: boolean;
places: boolean;
faces: boolean;
+75 -61
View File
@@ -17,6 +17,7 @@
import { dateTimeFormatFor, iconNameFromClass } from '$lib/utils/display';
import { userInitials, avatarColorIndex } from '$lib/utils/avatar';
import { i18n, LANGUAGES, setLocale, t, type Locale } from '$lib/i18n/index.svelte';
import { serverConfig } from '$lib/stores/serverConfig.svelte';
import { serverStatus } from '$lib/stores/serverStatus.svelte';
import { apiFetch } from '$lib/api/client';
import { dialogs } from '$lib/stores/dialogs.svelte';
@@ -74,68 +75,81 @@
// strip; that was displaced here so the section navigation
// scales past ~7 items and matches deep-link URLs from the
// address bar.
const ADMIN_LINKS: NavLink[] = [
{
href: '/admin',
label: t('admin.dashboard', 'Dashboard'),
icon: 'chart-pie',
section: 'admin-dashboard'
},
{
href: '/admin/users',
label: t('admin.users', 'Users'),
icon: 'users',
section: 'admin-users'
},
{
href: '/admin/sessions',
label: t('admin.sessions', 'Sessions'),
icon: 'key',
section: 'admin-sessions'
},
{
href: '/admin/drives',
label: t('admin.drives', 'Drives'),
icon: 'hdd',
section: 'admin-drives'
},
{
href: '/admin/mounts',
label: t('admin.mounts', 'External Mounts'),
icon: 'folder',
section: 'admin-mounts'
},
{
href: '/admin/oidc',
label: t('admin.oidc', 'OIDC / SSO'),
icon: 'building-shield',
section: 'admin-oidc'
},
{
href: '/admin/storage',
label: t('admin.storage_tab', 'Storage'),
icon: 'database',
section: 'admin-storage'
},
{
href: '/admin/smtp',
label: t('admin.smtp', 'Email (SMTP)'),
icon: 'envelope',
section: 'admin-smtp'
},
{
href: '/admin/plugins',
label: t('admin.plugins', 'Plugins'),
icon: 'layer-group',
section: 'admin-plugins'
},
{
href: '/admin/jobs',
label: t('admin.jobs.tab', 'Background tasks'),
icon: 'cogs',
section: 'admin-jobs'
// `$derived` so feature-flag gating drops entries when a feature is
// disabled server-side. Server-side the admin CRUD routes are also
// gated (matching the message-bus pattern) — hiding the link here
// keeps the sidebar consistent with what the backend actually
// serves; a stale link would land on a 404. See
// `$lib/stores/serverConfig.svelte.ts`.
const ADMIN_LINKS = $derived.by<NavLink[]>(() => {
const links: NavLink[] = [
{
href: '/admin',
label: t('admin.dashboard', 'Dashboard'),
icon: 'chart-pie',
section: 'admin-dashboard'
},
{
href: '/admin/users',
label: t('admin.users', 'Users'),
icon: 'users',
section: 'admin-users'
},
{
href: '/admin/sessions',
label: t('admin.sessions', 'Sessions'),
icon: 'key',
section: 'admin-sessions'
},
{
href: '/admin/drives',
label: t('admin.drives', 'Drives'),
icon: 'hdd',
section: 'admin-drives'
}
];
if (serverConfig.features.external_mounts) {
links.push({
href: '/admin/mounts',
label: t('admin.mounts', 'External Mounts'),
icon: 'folder',
section: 'admin-mounts'
});
}
];
links.push(
{
href: '/admin/oidc',
label: t('admin.oidc', 'OIDC / SSO'),
icon: 'building-shield',
section: 'admin-oidc'
},
{
href: '/admin/storage',
label: t('admin.storage_tab', 'Storage'),
icon: 'database',
section: 'admin-storage'
},
{
href: '/admin/smtp',
label: t('admin.smtp', 'Email (SMTP)'),
icon: 'envelope',
section: 'admin-smtp'
},
{
href: '/admin/plugins',
label: t('admin.plugins', 'Plugins'),
icon: 'layer-group',
section: 'admin-plugins'
},
{
href: '/admin/jobs',
label: t('admin.jobs.tab', 'Background tasks'),
icon: 'cogs',
section: 'admin-jobs'
}
);
return links;
});
const isAdmin = $derived(session.user?.role === 'admin');
@@ -36,7 +36,6 @@ const DEFAULT_FEATURES: ServerFeatures = {
trash: true,
search: true,
sharing: true,
quotas: false,
music: true,
places: true,
faces: false,
+79 -17
View File
@@ -83,6 +83,7 @@
} from '$lib/api/types';
import { shortUserAgent } from '$lib/utils/userAgent';
import { triggerJob } from '$lib/api/endpoints/adminJobs';
import { serverConfig } from '$lib/stores/serverConfig.svelte';
import { serverStatus } from '$lib/stores/serverStatus.svelte';
import AdminJobsPanel from '$lib/components/AdminJobsPanel.svelte';
import Icon from '$lib/icons/Icon.svelte';
@@ -224,6 +225,44 @@
// `$effect` loop is even possible.
const tab = $derived<Tab>(parseTab(page.params.tab));
/**
* Feature-flag matrix for the dashboard "System" section.
* Data-driven from `serverConfig.features` (populated at boot from
* `GET /api/config`). Each entry becomes one card; adding a
* feature server-side flows through this list automatically —
* label lookup falls back to the raw key so a missing translation
* won't hide the card.
*
* Uses `unknown` bracket-key reads (rather than a rigid mapping
* over hard-coded keys) so the FE doesn't need a code change when
* the backend adds a new feature flag. The i18n key namespace
* `admin.features.<key>` keeps translations discoverable.
*/
interface FeatureRow {
key: string;
label: string;
enabled: boolean;
}
const FEATURE_LABELS: Record<string, string> = {
message_bus: 'Message bus',
trash: 'Trash',
search: 'Search',
sharing: 'Sharing',
music: 'Music',
places: 'Places (photo map)',
faces: 'People (faces)',
video_thumbnails: 'Video thumbnails',
external_mounts: 'External mounts'
};
const featureRows = $derived.by<FeatureRow[]>(() => {
const raw = serverConfig.features as unknown as Record<string, boolean>;
return Object.entries(raw).map(([key, enabled]) => ({
key,
label: t(`admin.features.${key}`, FEATURE_LABELS[key] ?? key),
enabled
}));
});
/**
* Human-readable label for the current section — feeds the
* page title (`Admin › Jobs · OxiCloud`) and the h1. Kept in
@@ -1905,33 +1944,56 @@
</span>
{t('admin.online_sessions', 'Online sessions')}
</div>
<!-- WS-sessions card only rendered when the message bus is
enabled server-side. With `OXICLOUD_MESSAGEBUS_ENABLE=false`
the count is unconditionally 0, and showing "0 Live
WS sessions" reads like a bug when the feature simply
isn't running. `serverConfig.features.message_bus`
comes from `/api/config` at boot. -->
{#if serverConfig.features.message_bus}
<div
class="ds-card"
title={t(
'admin.active_ws_sessions_tooltip',
'Currently-connected message-bus WebSocket sessions — one per open browser tab reaching a folder view'
)}
>
<span class="ds-num ds-num--live">
<span class="presence-dot presence-dot--online" aria-hidden="true"></span>
{dashboard.active_ws_sessions}
</span>
{t('admin.active_ws_sessions', 'Live WS sessions')}
</div>
{/if}
</div>
<!-- Section 3: System — deployment flags + version. -->
<!-- Section 3: System — deployment flags + version.
Feature cards are data-driven from `/api/config`
(see `serverConfig.features` + `featureRows` derived).
Auth / OIDC / version still come from the dashboard
endpoint since those are per-deployment "system"
concerns not exposed on the public config surface.
Adding a new feature server-side flows into this grid
automatically — no template change needed. -->
<h2 class="ds-section-title">{t('admin.section_system', 'System')}</h2>
<div class="ds-grid">
<div class="ds-card">
<span class="ds-flag" class:ds-flag--on={dashboard.auth_enabled}>
{dashboard.auth_enabled
? t('admin.enabled', 'Enabled')
: t('admin.disabled', 'Disabled')}
</span>
{t('admin.auth', 'Authentication')}
</div>
<div class="ds-card">
<span class="ds-flag" class:ds-flag--on={dashboard.oidc_configured}>
{dashboard.oidc_configured ? t('admin.active', 'Active') : t('admin.off', 'Off')}
</span>
{t('admin.oidc', 'OIDC / SSO')}
</div>
<div class="ds-card">
<span class="ds-flag" class:ds-flag--on={dashboard.quotas_enabled}>
{dashboard.quotas_enabled
? t('admin.enabled', 'Enabled')
: t('admin.disabled', 'Disabled')}
</span>
{t('admin.quotas', 'Quotas')}
</div>
{#each featureRows as row (row.key)}
<div class="ds-card" data-testid="admin-feature-card-{row.key}">
<span class="ds-flag" class:ds-flag--on={row.enabled}>
{row.enabled ? t('admin.enabled', 'Enabled') : t('admin.disabled', 'Disabled')}
</span>
{row.label}
</div>
{/each}
<!-- Version card intentionally last — tertiary build
metadata (like a footer), least useful at a glance
compared to the feature-toggle cards above. -->
<div class="ds-card">
<span class="ds-num">v{dashboard.server_version}</span>{t('admin.version', 'Version')}
</div>
@@ -89,9 +89,8 @@ const dashboard = {
total_used_bytes: 100,
total_quota_bytes: 1000,
storage_usage_percent: 10,
auth_enabled: true,
oidc_configured: false,
quotas_enabled: true,
active_ws_sessions: 0,
registration_enabled: true,
users_over_80_percent: 0,
users_over_quota: 0