{t('admin.title', 'Admin')} · OxiCloud {#snippet envBadge(on: boolean)} {#if on} ENV {/if} {/snippet}

{t('admin.title', 'Admin')}

{#if tab === 'dashboard'} {#if dashboardError}

{dashboardError}

{:else if !dashboard}

{t('common.loading', 'Loading…')}

{:else}
{dashboard.total_users}{t('admin.total_users', 'Total users')}
{dashboard.active_users}{t('admin.active_users', 'Active')}
{dashboard.admin_users}{t('admin.admin_users', 'Admins')}
v{dashboard.server_version}{t('admin.version', 'Version')}
{dashboard.auth_enabled ? t('admin.enabled', 'Enabled') : t('admin.disabled', 'Disabled')} {t('admin.auth', 'Authentication')}
{dashboard.oidc_configured ? t('admin.active', 'Active') : t('admin.off', 'Off')} {t('admin.oidc', 'OIDC / SSO')}
{dashboard.quotas_enabled ? t('admin.enabled', 'Enabled') : t('admin.disabled', 'Disabled')} {t('admin.quotas', 'Quotas')}
{#if dashboard.users_over_quota > 0}
{dashboard.users_over_quota} {t('admin.over_quota', { n: dashboard.users_over_quota }, '{{n}} users over quota')}
{/if} {#if dashboard.users_over_80_percent > 0}
{dashboard.users_over_80_percent} {t( 'admin.over_80', { n: dashboard.users_over_80_percent }, '{{n}} users over 80% quota' )}
{/if}

{t('admin.storage', 'Storage')}

70} class:ds-fill--danger={dashboard.storage_usage_percent > 90} style:width="{Math.min(dashboard.storage_usage_percent, 100)}%" >

{formatBytes(dashboard.total_used_bytes)} / {formatBytes(dashboard.total_quota_bytes)} ({dashboard.storage_usage_percent.toFixed(1)}%)

{#if dashboard.registration_enabled !== undefined}

{t('admin.registration', 'Registration')}

{#if !dashboard.registration_enabled}

{t( 'admin.registration_disabled_warning', 'Public registration is disabled. Only admins can create new accounts.' )}

{/if}
{/if} {/if} {:else if tab === 'oidc'}

{t('admin.oidc', 'OIDC / SSO')}

{#if !oidc}

{t('common.loading', 'Loading…')}

{:else}
(e.preventDefault(), doSaveOidc())}> {#if oidcTest}
{oidcTest.message} {#if oidcTest.success}
{t('admin.oidc_issuer', 'Issuer URL')}
{oidcTest.issuer || '—'}
{t('admin.oidc_auth_endpoint', 'Auth endpoint')}
{oidcTest.authorization_endpoint || '—'}
{/if}
{/if} {#if oidc.callback_url}

{t('admin.oidc_callback', 'Callback URL')}: {oidc.callback_url}

{/if} {#if oidcMsg}

{oidcMsg.text}

{/if}
{/if}
{:else if tab === 'storage'}

{t('admin.storage_tab', 'Storage')}

{#if !storage}

{t('common.loading', 'Loading…')}

{:else}
(e.preventDefault(), doSaveStorage())}> {#if sForm.backend === 's3'} {/if} {#if storageMsg}

{storageMsg.text}

{/if}
{#if sForm.backend === 's3'} {/if}
{t('admin.storage_current', 'Current backend')}
{storage.current_backend ?? '—'}
{t('admin.storage_blobs', 'Blobs')}
{storage.total_blobs ?? '—'}
{t('admin.storage_size', 'Stored')}
{storage.total_bytes_stored != null ? formatBytes(storage.total_bytes_stored) : '—'}
{t('admin.storage_dedup', 'Dedup ratio')}
{storage.dedup_ratio != null ? `${storage.dedup_ratio.toFixed(2)}x` : '—'}
{/if}

{t('admin.migration', 'Storage migration')}

{#if !migration}

{t('common.loading', 'Loading…')}

{:else}

{t('admin.status', 'Status')}: {migration.status}

{#if migration.total_blobs > 0}

{migration.migrated_blobs} / {migration.total_blobs} ({migrationPct}%) · {formatBytes(migration.migrated_bytes)} {#if migration.throughput_bytes_per_sec && migration.status === 'running'} · {formatBytes(Math.round(migration.throughput_bytes_per_sec))}/s {/if} {#if migrationEtaMin != null} · {t('admin.mig_eta', { min: migrationEtaMin }, `~${migrationEtaMin} min remaining`)} {/if}

{/if} {#if migration.failed_blobs && migration.failed_blobs.length > 0}
{t( 'admin.mig_failed', { n: migration.failed_blobs.length }, `${migration.failed_blobs.length} failed blobs` )}
{migration.failed_blobs.join('\n')}
{/if}
{#if migration.status !== 'running' && migration.status !== 'paused' && migration.status !== 'completed'} {/if} {#if migration.status === 'running'} {/if} {#if migration.status === 'paused'} {/if} {#if migration.status === 'completed'} {/if}
{#if verifyError}
{verifyError}
{:else if verifyResult}
{verifyResult.passed ? t('admin.mig_verify_passed', 'Verification passed') : t('admin.mig_verify_failed', 'Verification failed')} {#if verifyResult.passed}

{t( 'admin.mig_verify_summary', { checked: verifyResult.sample_checked, total: verifyResult.pg_blob_count }, '{{checked}} blobs checked, {{total}} total in database' )}

{:else}

{[ verifyResult.missing_in_target.length ? t( 'admin.mig_verify_missing', { n: verifyResult.missing_in_target.length }, '{{n}} missing' ) : '', verifyResult.size_mismatches.length ? t( 'admin.mig_verify_mismatch', { n: verifyResult.size_mismatches.length }, '{{n}} size mismatches' ) : '' ] .filter(Boolean) .join(', ')}

{/if}
{/if} {/if}
{:else if tab === 'smtp'}

{t('admin.smtp_status', 'SMTP status')}

{#if !smtp}

{t('common.loading', 'Loading…')}

{:else}
{t('admin.smtp_enabled', 'Enabled')}
{smtp.enabled ? t('common.yes', 'Yes') : t('common.no', 'No')}
{t('admin.smtp_host', 'Host')}
{smtp.host || '—'}
{t('admin.smtp_port', 'Port')}
{smtp.port || '—'}
TLS
{smtp.tls || '—'}
{t('admin.smtp_from', 'From')}
{smtp.from || '—'}
{t('admin.smtp_user_state', 'Auth')}
{smtp.user_state || '—'}
{/if}

{t('admin.smtp_test', 'Send test email')}

{#if smtpResult} {#if smtpResult.success}

{t('admin.smtp_sent', 'Test email sent.')}
{t('admin.smtp_server_code', 'Server replied')}: {smtpResult.code ?? ''} {smtpResult.message ?? ''}

{:else}

{t('admin.smtp_fail', 'Send failed.')}
{smtpResult.error || smtpResult.message || t('common.error', 'unknown error')}

{/if} {/if}
{:else if tab === 'users'}
{#if usersError}

{usersError}

{:else} {#each users as u (u.id)} {@const pct = quotaPct(u)} {/each}
{t('admin.user', 'User')} {t('admin.role', 'Role')} {t('admin.auth', 'Auth')} {t('admin.status', 'Status')} {t('admin.quota', 'Storage usage')} {t('admin.last_login', 'Last login')}
{u.username || u.email} {#if isSelf(u)} {t('admin.you_badge', 'you')} {/if} {u.email}
{#if u.role === 'admin'}{/if} {u.role} {#if isOidcUser(u)} {u.auth_provider} {:else} {t('admin.local', 'local')} {/if} {u.active ? t('admin.active', 'Active') : t('admin.inactive', 'Inactive')}
70} class:quota-fill--danger={pct > 90} style:width="{Math.min(pct, 100)}%" >
{formatBytes(u.storage_used_bytes)} / {u.storage_quota_bytes > 0 ? formatBytes(u.storage_quota_bytes) : '∞'}
{timeAgo(u.last_login_at)} {#if !isOidcUser(u)} {/if}
{pageIndex + 1} / {Math.max(1, Math.ceil(total / PAGE_SIZE))}
{/if} {:else if !pluginsAvailable}

{t('admin.plugins_disabled', 'The plugin subsystem is disabled.')}

{:else if pluginsError}

{pluginsError}

{:else}
{t('admin.plugins_install', 'Install plugin')} {t('admin.plugins_install_hint', 'Upload a plugin bundle (.zip).')}
{#if installMsg}

{installMsg.text}

{/if} {#if plugins.length === 0}

{t('admin.no_plugins', 'No plugins installed.')}

{:else} {#each plugins as p (p.id)} {/each}
{t('admin.plugin', 'Plugin')} {t('admin.plugins_col_id', 'ID')} {t('admin.version', 'Version')} {t('admin.plugins_col_events', 'Events')} {t('admin.status', 'Status')}
{p.name} {#if p.description}{p.description}{/if}
{p.id} {p.version ?? '—'} {#if p.subscriptions && p.subscriptions.length > 0} {p.subscriptions.length} {:else} — {/if} {p.enabled ? t('admin.enabled', 'Enabled') : t('admin.disabled', 'Disabled')}
{/if} {/if}
{#if createError}

{createError}

{/if}
{#snippet footer()} {/snippet}
(quotaModal = null)} > {#if quotaModal}
{ e.preventDefault(); void saveQuota(); }} >

{t('admin.quota_for', 'Quota for')} {quotaModal.username}

{/if} {#snippet footer()} {/snippet}
(resetModal = null)} > {#if resetModal}

{t('admin.reset_pw_for', 'New password for')} {resetModal.username}

{#if resetError}

{resetError}

{/if}
{/if} {#snippet footer()} {/snippet}
resolveConfirm(false)} >

{confirmState?.message}

{#snippet footer()} {/snippet}
{#if logsPlugin}
{t('admin.plugins_col_id', 'ID')}
{logsPlugin.id}
{t('admin.version', 'Version')}
{logsPlugin.version ?? '—'}
{#if logsPlugin.abi != null}
ABI
{logsPlugin.abi}
{/if}
{t('admin.plugins_col_events', 'Events')}
{#if logsPlugin.subscriptions && logsPlugin.subscriptions.length > 0} {#each logsPlugin.subscriptions as ev (ev)}{ev} {/each} {:else} — {/if}
{t('admin.status', 'Status')}
{logsPlugin.enabled ? t('admin.enabled', 'Enabled') : t('admin.disabled', 'Disabled')}
{/if} {#if retention}
(e.preventDefault(), saveRetention())}>

{t('admin.plugins_retention', 'Log retention')}

{#if retentionMsg}

{retentionMsg}

{/if}
{/if}
e.key === 'Enter' && reloadLogsFromStart()} />
{#if logsLoading}

{t('common.loading', 'Loading…')}

{:else if logs.length === 0}

{t('admin.logs_empty', 'No log entries.')}

{:else}
{#each logs as entry, i (i)} {/each}
{t('admin.logs_time', 'Time')} {t('admin.logs_level', 'Level')} {t('admin.logs_kind', 'Kind')} {t('admin.logs_invocation', 'Invocation')} {t('admin.logs_message', 'Message')}
{timeAgo(entry.ts ?? entry.timestamp)} {entry.level ?? 'info'} {logKind(entry)} {entry.invocation_id ?? '—'} {logMsg(entry)}
{/if}
{#if logsTotal === 0} {t('admin.logs_empty', 'No log entries.')} {:else} {t( 'admin.logs_showing', { from: logsPage * LOGS_PAGE_SIZE + 1, to: Math.min((logsPage + 1) * LOGS_PAGE_SIZE, logsTotal), total: logsTotal }, 'Showing {{from}}–{{to}} of {{total}}' )} {/if}
{#snippet footer()} {/snippet}