81a93a489b
Backend — Photos timeline now groups by real capture date instead of upload time. New MediaMetadataService (FileLifecycleHook) extracts EXIF DateTimeOriginal from images and container creation_time from videos (mov/mp4/mkv) via nom-exif, timezone-correct (OffsetTimeOriginal), persisting captured_at so the existing media_sort_date trigger takes over. Adds POST /admin/photos/metadata/reextract to backfill existing media. Falls back to upload date when no embedded date exists. Frontend — premium grid cards: combined metadata line (relative date · size, owner avatar when shared), custom selection checkbox with a clear checked state, uniform full-width 4:3 thumbnail tiles independent of filename length, centered file-type icons, and a hit-test fix so checkbox/star/kebab clicks reach the controls (the decorative thumbnail no longer captures pointer events). Notification messages internationalised across all 16 locales. Broader polish: design tokens, a11y/focus-visible states, brand + PWA assets. Chore — bump semver-compatible dependencies (cargo upgrade); add nom-exif 3.6.1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
863 lines
46 KiB
HTML
863 lines
46 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<!-- Declares the page supports both modes. Switching between them is
|
||
driven by the CSS `color-scheme` property (via the `data-color-scheme`
|
||
attribute on <html> set by theme-init.js), not by mutating this tag. -->
|
||
<meta name="color-scheme" content="light dark" />
|
||
<title>OxiCloud — Admin Panel</title>
|
||
<link rel="icon" type="image/svg+xml" href="/logo/logo-plain.svg">
|
||
<script src="/js/core/theme-init.js"></script>
|
||
<link rel="stylesheet" href="/css/main.css" />
|
||
<link rel="stylesheet" href="/css/views/admin.css" />
|
||
</head>
|
||
<body>
|
||
<div class="admin-header">
|
||
<div class="admin-header-left">
|
||
<a href="/" class="admin-logo-link link-reset-flex">
|
||
<div class="admin-logo">
|
||
<svg viewBox="0 0 500 500">
|
||
<path
|
||
d="M345 310c32 0 58-26 58-58s-26-58-58-58c-6.2 0-12 0.9-17.5 2.7C318 166 289 143 255 143c-34.3 0-63.1 22.6-73 53.7C176.9 195.7 171 195 165 195c-32 0-58 26-58 58s26 58 58 58h180z"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
<span class="admin-title-text">OxiCloud</span>
|
||
</a>
|
||
<span class="admin-title-separator">· <span data-i18n="admin.tab_dashboard">Admin</span></span>
|
||
</div>
|
||
<div class="admin-header-right">
|
||
<a href="/"
|
||
><i class="fas fa-arrow-left"></i> <span data-i18n="admin.back_to_app">Back to OxiCloud</span></a
|
||
>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="admin-container" role="main">
|
||
<h1 class="sr-only" data-i18n="user_menu.admin_panel">Admin panel</h1>
|
||
<div id="loading"><i class="fas fa-circle-notch"></i> <span data-i18n="admin.loading">Loading…</span></div>
|
||
<div id="access-denied">
|
||
<div class="access-icon"><i class="fas fa-lock"></i></div>
|
||
<h2 data-i18n="admin.access_denied">Access Denied</h2>
|
||
<p data-i18n="admin.access_denied_desc">Administrator privileges required to access this panel.</p>
|
||
<a href="/login"><i class="fas fa-sign-in-alt"></i> <span data-i18n="admin.sign_in">Sign in</span></a>
|
||
</div>
|
||
|
||
<div id="main-content">
|
||
<div class="admin-tabs">
|
||
<button
|
||
class="admin-tab active"
|
||
id="tab-btn-dashboard"
|
||
>
|
||
<i class="fas fa-chart-pie"></i> <span data-i18n="admin.tab_dashboard">Dashboard</span>
|
||
</button>
|
||
<button class="admin-tab" id="tab-btn-users">
|
||
<i class="fas fa-users"></i> <span data-i18n="admin.tab_users">Users</span>
|
||
</button>
|
||
<button class="admin-tab" id="tab-btn-oidc">
|
||
<i class="fas fa-key"></i> <span data-i18n="admin.tab_oidc">SSO / OIDC</span>
|
||
</button>
|
||
<button class="admin-tab" id="tab-btn-storage">
|
||
<i class="fas fa-database"></i> <span data-i18n="admin.tab_storage">Storage</span>
|
||
</button>
|
||
<button class="admin-tab" id="tab-btn-smtp">
|
||
<i class="fas fa-envelope"></i> <span data-i18n="admin.tab_smtp">SMTP</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div id="tab-dashboard" class="tab-content active">
|
||
<div class="stats-grid">
|
||
<div class="stat-card">
|
||
<div
|
||
class="stat-value text-blue"
|
||
id="ds-total-users"
|
||
>
|
||
—
|
||
</div>
|
||
<div class="stat-label" data-i18n="admin.total_users">Total Users</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div
|
||
class="stat-value text-green"
|
||
id="ds-active-users"
|
||
>
|
||
—
|
||
</div>
|
||
<div class="stat-label" data-i18n="admin.active_users">Active Users</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div
|
||
class="stat-value text-blue"
|
||
id="ds-admin-users"
|
||
>
|
||
—
|
||
</div>
|
||
<div class="stat-label" data-i18n="admin.admins">Admins</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="ds-version">—</div>
|
||
<div class="stat-label" data-i18n="admin.version">Version</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="admin-card">
|
||
<h2><i class="fas fa-hdd"></i> <span data-i18n="admin.storage_overview">Storage Overview</span></h2>
|
||
<div class="stats-grid">
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="ds-used">—</div>
|
||
<div class="stat-label" data-i18n="admin.used">Used</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="ds-quota">—</div>
|
||
<div class="stat-label" data-i18n="admin.total_quota">Total Quota</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="ds-usage-pct">
|
||
—
|
||
</div>
|
||
<div class="stat-label" data-i18n="admin.usage_pct">Usage %</div>
|
||
</div>
|
||
</div>
|
||
<div class="progress-bar">
|
||
<div
|
||
class="progress-fill green width-zero"
|
||
id="ds-bar"
|
||
></div>
|
||
</div>
|
||
<div class="mt-14">
|
||
<div class="stats-grid">
|
||
<div class="stat-card warn" id="ds-warn-card">
|
||
<div
|
||
class="stat-value text-orange"
|
||
id="ds-over80"
|
||
>
|
||
0
|
||
</div>
|
||
<div class="stat-label" data-i18n="admin.users_over_80">
|
||
Users >80% quota
|
||
</div>
|
||
</div>
|
||
<div
|
||
class="stat-card danger"
|
||
id="ds-danger-card"
|
||
>
|
||
<div
|
||
class="stat-value text-red"
|
||
id="ds-overquota"
|
||
>
|
||
0
|
||
</div>
|
||
<div class="stat-label" data-i18n="admin.users_over_quota">
|
||
Users over quota
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="admin-card">
|
||
<h2><i class="fas fa-server"></i> <span data-i18n="admin.system">System</span></h2>
|
||
<div class="stats-grid">
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="ds-auth">—</div>
|
||
<div class="stat-label" data-i18n="admin.auth_label">Auth</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="ds-oidc">—</div>
|
||
<div class="stat-label" data-i18n="admin.oidc_label">OIDC</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="ds-quotas-flag">
|
||
—
|
||
</div>
|
||
<div class="stat-label" data-i18n="admin.quotas_label">Quotas</div>
|
||
</div>
|
||
</div>
|
||
<div class="toggle-row toggle-row-strong">
|
||
<label
|
||
><i
|
||
class="fas fa-user-plus icon-muted-right"
|
||
></i>
|
||
<span data-i18n="admin.allow_registration">Allow public self-registration</span></label
|
||
>
|
||
<label class="switch"
|
||
><input
|
||
type="checkbox"
|
||
id="ds-registration"
|
||
checked /><span
|
||
class="slider"
|
||
></span
|
||
></label>
|
||
</div>
|
||
<div class="warning" id="registration-warning">
|
||
<i class="fas fa-exclamation-triangle"></i> <span data-i18n="admin.registration_warning">Public registration is disabled. Only admins can create new users.</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="tab-users" class="tab-content">
|
||
<div class="admin-card">
|
||
<h2 class="h2-space-between">
|
||
<span
|
||
><i class="fas fa-users-cog"></i> <span data-i18n="admin.user_management">User Management</span></span
|
||
><button
|
||
class="btn btn-primary"
|
||
id="btn-create-user"
|
||
>
|
||
<i class="fas fa-user-plus"></i> <span data-i18n="admin.create_user">Create User</span>
|
||
</button>
|
||
</h2>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th data-i18n="admin.col_user">User</th>
|
||
<th data-i18n="admin.col_role">Role</th>
|
||
<th data-i18n="admin.col_auth">Auth</th>
|
||
<th data-i18n="admin.col_status">Status</th>
|
||
<th data-i18n="admin.col_storage">Storage</th>
|
||
<th data-i18n="admin.col_last_login">Last Login</th>
|
||
<th data-i18n="admin.col_actions">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="users-tbody">
|
||
<tr>
|
||
<td
|
||
colspan="7"
|
||
class="table-loading-cell"
|
||
>
|
||
<i
|
||
class="fas fa-spinner fa-spin"
|
||
></i>
|
||
<span data-i18n="admin.loading_users">Loading users…</span>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="pagination">
|
||
<span id="users-info">—</span>
|
||
<div class="flex-gap-6">
|
||
<button
|
||
class="btn btn-sm btn-secondary"
|
||
id="prev-btn"
|
||
disabled
|
||
>
|
||
<i class="fas fa-chevron-left"></i> <span data-i18n="admin.prev">Prev</span>
|
||
</button>
|
||
<button
|
||
class="btn btn-sm btn-secondary"
|
||
id="next-btn"
|
||
>
|
||
<span data-i18n="admin.next">Next</span> <i class="fas fa-chevron-right"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="tab-oidc" class="tab-content">
|
||
<div class="admin-card">
|
||
<h2>
|
||
<i class="fas fa-shield-alt"></i> <span data-i18n="admin.sso_title">Single Sign-On (OIDC / SSO)</span>
|
||
</h2>
|
||
<div class="toggle-row">
|
||
<label data-i18n="admin.enable_sso">Enable SSO Authentication</label>
|
||
<label class="switch"
|
||
><input
|
||
type="checkbox"
|
||
id="oidc-enabled" /><span
|
||
class="slider"
|
||
></span
|
||
></label>
|
||
</div>
|
||
<div id="oidc-form">
|
||
<div class="form-group">
|
||
<label
|
||
><span data-i18n="admin.provider_name">Provider Name</span>
|
||
<span id="badge-provider_name"></span
|
||
></label>
|
||
<input
|
||
type="text"
|
||
id="provider-name"
|
||
placeholder="e.g., Authentik, Keycloak"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label
|
||
><span data-i18n="admin.issuer_url">Issuer URL</span>
|
||
<span id="badge-issuer_url"></span
|
||
></label>
|
||
<input
|
||
type="url"
|
||
id="issuer-url"
|
||
placeholder="https://auth.example.com/application/o/oxicloud/"
|
||
/>
|
||
<small data-i18n="admin.issuer_url_hint"
|
||
>OpenID Connect issuer URL of your identity
|
||
provider</small
|
||
>
|
||
</div>
|
||
<div class="oidc-discover-wrap">
|
||
<button
|
||
class="btn btn-secondary btn-sm"
|
||
id="discover-btn"
|
||
>
|
||
<i class="fas fa-search"></i> <span data-i18n="admin.auto_discover">Auto-discover</span>
|
||
</button>
|
||
</div>
|
||
<div id="discovery-result"></div>
|
||
<div class="form-group">
|
||
<label
|
||
><span data-i18n="admin.client_id">Client ID</span> <span id="badge-client_id"></span
|
||
></label>
|
||
<input
|
||
type="text"
|
||
id="client-id"
|
||
placeholder="oxicloud"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label
|
||
><span data-i18n="admin.client_secret">Client Secret</span>
|
||
<span id="badge-client_secret"></span
|
||
></label>
|
||
<input
|
||
type="password"
|
||
id="client-secret"
|
||
placeholder="Leave empty to keep current value"
|
||
data-i18n-placeholder="admin.client_secret_placeholder"
|
||
/>
|
||
<small id="secret-hint"
|
||
><i
|
||
class="fas fa-check-circle secret-icon"
|
||
></i>
|
||
<span data-i18n="admin.secret_configured">A client secret is already configured</span></small
|
||
>
|
||
</div>
|
||
<div class="form-group">
|
||
<label
|
||
><span data-i18n="admin.callback_url">Callback URL</span>
|
||
<small class="small-muted" data-i18n="admin.callback_url_hint"
|
||
>(register in your IdP)</small
|
||
></label
|
||
>
|
||
<div class="readonly-field">
|
||
<span id="callback-url">—</span
|
||
><button
|
||
id="btn-copy-callback"
|
||
title="Copy"
|
||
>
|
||
<i class="fas fa-copy"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<details>
|
||
<summary>
|
||
<i
|
||
class="fas fa-sliders-h summary-icon-right"
|
||
></i>
|
||
<span data-i18n="admin.advanced_settings">Advanced Settings</span>
|
||
</summary>
|
||
<div class="form-group">
|
||
<label
|
||
><span data-i18n="admin.scopes">Scopes</span> <span id="badge-scopes"></span>
|
||
></label>
|
||
<input
|
||
type="text"
|
||
id="scopes"
|
||
placeholder="openid profile email"
|
||
/>
|
||
</div>
|
||
<div class="toggle-row">
|
||
<label data-i18n="admin.auto_provision"
|
||
>Auto-provision users on first
|
||
login</label
|
||
>
|
||
<label class="switch"
|
||
><input
|
||
type="checkbox"
|
||
id="auto-provision"
|
||
checked /><span
|
||
class="slider"
|
||
></span
|
||
></label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label
|
||
><span data-i18n="admin.admin_groups">Admin Groups</span>
|
||
<span id="badge-admin_groups"></span>
|
||
></label>
|
||
<input
|
||
type="text"
|
||
id="admin-groups"
|
||
placeholder="e.g., oxicloud-admins"
|
||
/>
|
||
<small data-i18n="admin.admin_groups_hint"
|
||
>Comma-separated OIDC group names that
|
||
map to admin role</small
|
||
>
|
||
</div>
|
||
<div class="toggle-row">
|
||
<label data-i18n="admin.disable_password"
|
||
>Disable password login (OIDC
|
||
only)</label
|
||
>
|
||
<label class="switch"
|
||
><input
|
||
type="checkbox"
|
||
id="disable-password" /><span
|
||
class="slider"
|
||
></span
|
||
></label>
|
||
</div>
|
||
<div class="warning" id="password-warning">
|
||
<i class="fas fa-exclamation-triangle"></i>
|
||
<span data-i18n="admin.password_warning">This will prevent ALL password-based logins!</span>
|
||
</div>
|
||
</details>
|
||
<div class="oidc-actions">
|
||
<button
|
||
class="btn btn-secondary"
|
||
id="btn-test-oidc"
|
||
>
|
||
<i class="fas fa-vial"></i> <span data-i18n="admin.test_btn">Test</span>
|
||
</button>
|
||
<button
|
||
class="btn btn-primary"
|
||
id="save-btn"
|
||
>
|
||
<i class="fas fa-save"></i> <span data-i18n="admin.save_btn">Save</span>
|
||
</button>
|
||
</div>
|
||
<div id="oidc-status" class="alert"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ════════ Storage tab ════════ -->
|
||
<div id="tab-storage" class="tab-content">
|
||
<div class="admin-card">
|
||
<h2>
|
||
<i class="fas fa-database"></i> <span data-i18n="admin.storage_title">Storage Backend</span>
|
||
</h2>
|
||
|
||
<!-- Current status -->
|
||
<div class="storage-status-grid">
|
||
<div class="storage-stat">
|
||
<span class="storage-stat__label" data-i18n="admin.storage_current_backend">Active Backend</span>
|
||
<span class="storage-stat__value" id="storage-current-backend">—</span>
|
||
</div>
|
||
<div class="storage-stat">
|
||
<span class="storage-stat__label" data-i18n="admin.storage_total_blobs">Total Blobs</span>
|
||
<span class="storage-stat__value" id="storage-total-blobs">—</span>
|
||
</div>
|
||
<div class="storage-stat">
|
||
<span class="storage-stat__label" data-i18n="admin.storage_total_size">Total Size</span>
|
||
<span class="storage-stat__value" id="storage-total-size">—</span>
|
||
</div>
|
||
<div class="storage-stat">
|
||
<span class="storage-stat__label" data-i18n="admin.storage_dedup_ratio">Dedup Ratio</span>
|
||
<span class="storage-stat__value" id="storage-dedup-ratio">—</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Backend selector -->
|
||
<div class="storage-backend-selector">
|
||
<label class="storage-backend-option">
|
||
<input type="radio" name="storage-backend" value="local" checked />
|
||
<span class="storage-backend-option__card">
|
||
<i class="fas fa-hdd"></i>
|
||
<span data-i18n="admin.storage_local">Local Filesystem</span>
|
||
</span>
|
||
</label>
|
||
<label class="storage-backend-option">
|
||
<input type="radio" name="storage-backend" value="s3" />
|
||
<span class="storage-backend-option__card">
|
||
<i class="fas fa-cloud"></i>
|
||
<span data-i18n="admin.storage_s3">S3-Compatible</span>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
|
||
<!-- S3 form (shown when S3 is selected) -->
|
||
<div id="storage-s3-form" class="storage-form" style="display:none">
|
||
<!-- Provider preset -->
|
||
<div class="form-group">
|
||
<label data-i18n="admin.storage_provider_preset">Provider Preset</label>
|
||
<select id="storage-preset">
|
||
<option value="custom" data-i18n="admin.storage_preset_custom">Custom</option>
|
||
<option value="aws">Amazon S3</option>
|
||
<option value="backblaze">Backblaze B2</option>
|
||
<option value="cloudflare-r2">Cloudflare R2</option>
|
||
<option value="minio">MinIO</option>
|
||
<option value="digitalocean">DigitalOcean Spaces</option>
|
||
<option value="wasabi">Wasabi</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Endpoint URL -->
|
||
<div class="form-group">
|
||
<label><span data-i18n="admin.storage_endpoint_url">Endpoint URL</span>
|
||
<span id="badge-s3_endpoint_url"></span></label>
|
||
<input type="url" id="storage-endpoint-url" placeholder="https://s3.amazonaws.com" />
|
||
<small data-i18n="admin.storage_endpoint_hint">Leave empty for Amazon S3 default</small>
|
||
</div>
|
||
|
||
<!-- Bucket -->
|
||
<div class="form-group">
|
||
<label><span data-i18n="admin.storage_bucket">Bucket</span>
|
||
<span id="badge-s3_bucket"></span></label>
|
||
<input type="text" id="storage-bucket" placeholder="my-oxicloud-bucket" />
|
||
</div>
|
||
|
||
<!-- Region -->
|
||
<div class="form-group">
|
||
<label><span data-i18n="admin.storage_region">Region</span>
|
||
<span id="badge-s3_region"></span></label>
|
||
<input type="text" id="storage-region" placeholder="us-east-1" />
|
||
</div>
|
||
|
||
<!-- Access Key -->
|
||
<div class="form-group">
|
||
<label><span data-i18n="admin.storage_access_key">Access Key ID</span>
|
||
<span id="badge-s3_access_key"></span></label>
|
||
<input type="text" id="storage-access-key" placeholder="AKIA..." />
|
||
</div>
|
||
|
||
<!-- Secret Key -->
|
||
<div class="form-group">
|
||
<label><span data-i18n="admin.storage_secret_key">Secret Access Key</span>
|
||
<span id="badge-s3_secret_key"></span></label>
|
||
<input type="password" id="storage-secret-key" placeholder="Leave empty to keep current value" />
|
||
<small id="storage-secret-hint" style="display:none">
|
||
<i class="fas fa-check-circle secret-icon"></i>
|
||
<span data-i18n="admin.storage_secret_configured">A secret key is already configured</span>
|
||
</small>
|
||
</div>
|
||
|
||
<!-- Force Path Style -->
|
||
<div class="toggle-row">
|
||
<label data-i18n="admin.storage_path_style">Force Path Style</label>
|
||
<label class="switch">
|
||
<input type="checkbox" id="storage-path-style" /><span class="slider"></span>
|
||
</label>
|
||
</div>
|
||
<small data-i18n="admin.storage_path_style_hint">Required for MinIO and some S3-compatible providers</small>
|
||
</div>
|
||
|
||
<!-- Action buttons -->
|
||
<div class="storage-actions">
|
||
<button class="btn btn-secondary" id="btn-test-storage">
|
||
<i class="fas fa-vial"></i> <span data-i18n="admin.storage_test_connection">Test Connection</span>
|
||
</button>
|
||
<button class="btn btn-primary" id="btn-save-storage">
|
||
<i class="fas fa-save"></i> <span data-i18n="admin.storage_save">Save</span>
|
||
</button>
|
||
</div>
|
||
<div id="storage-status" class="alert"></div>
|
||
|
||
<!-- Migration section -->
|
||
<div class="storage-migration-section">
|
||
<h3><i class="fas fa-exchange-alt"></i> <span data-i18n="admin.storage_migration">Backend Migration</span></h3>
|
||
|
||
<!-- Status badge -->
|
||
<div class="migration-status-row">
|
||
<span data-i18n="admin.migration_status_label">Status:</span>
|
||
<span id="migration-status-badge" class="badge badge-migration badge-migration--idle">Idle</span>
|
||
</div>
|
||
|
||
<!-- Progress bar (hidden when idle) -->
|
||
<div id="migration-progress-section" style="display:none">
|
||
<div class="migration-progress-bar">
|
||
<div class="migration-progress-bar__fill" id="migration-progress-fill" style="width:0%"></div>
|
||
</div>
|
||
<div class="migration-progress-text">
|
||
<span id="migration-progress-label">0 / 0 blobs (0%)</span>
|
||
<span id="migration-throughput"></span>
|
||
</div>
|
||
<div class="migration-progress-text">
|
||
<span id="migration-bytes-label"></span>
|
||
<span id="migration-eta"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Failed blobs (expandable) -->
|
||
<details id="migration-failed-section" style="display:none">
|
||
<summary><i class="fas fa-exclamation-triangle"></i> <span id="migration-failed-count">0</span> <span data-i18n="admin.migration_failed_blobs">failed blobs</span></summary>
|
||
<pre id="migration-failed-list" class="migration-failed-list"></pre>
|
||
</details>
|
||
|
||
<!-- Action buttons -->
|
||
<div class="migration-actions">
|
||
<button class="btn btn-primary" id="btn-start-migration">
|
||
<i class="fas fa-play"></i> <span data-i18n="admin.migration_start">Start Migration</span>
|
||
</button>
|
||
<button class="btn btn-secondary" id="btn-pause-migration" style="display:none">
|
||
<i class="fas fa-pause"></i> <span data-i18n="admin.migration_pause">Pause</span>
|
||
</button>
|
||
<button class="btn btn-primary" id="btn-resume-migration" style="display:none">
|
||
<i class="fas fa-play"></i> <span data-i18n="admin.migration_resume">Resume</span>
|
||
</button>
|
||
<button class="btn btn-secondary" id="btn-verify-migration" style="display:none">
|
||
<i class="fas fa-check-double"></i> <span data-i18n="admin.migration_verify">Verify Integrity</span>
|
||
</button>
|
||
<button class="btn btn-primary" id="btn-complete-migration" style="display:none">
|
||
<i class="fas fa-flag-checkered"></i> <span data-i18n="admin.migration_complete">Finalize</span>
|
||
</button>
|
||
</div>
|
||
<div id="migration-verify-result" style="display:none"></div>
|
||
<div id="migration-status-msg" class="alert" style="display:none"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ════════ SMTP tab ════════ -->
|
||
<div id="tab-smtp" class="tab-content">
|
||
<div class="admin-card">
|
||
<h2>
|
||
<i class="fas fa-envelope"></i> <span data-i18n="admin.smtp_title">Outbound Email (SMTP)</span>
|
||
</h2>
|
||
<p class="muted" data-i18n="admin.smtp_intro">
|
||
SMTP is configured exclusively via environment variables (OXICLOUD_SMTP_*). The values below are read from the running server — to change them, edit the environment and restart OxiCloud.
|
||
</p>
|
||
|
||
<table class="smtp-info-table">
|
||
<tbody>
|
||
<tr>
|
||
<th data-i18n="admin.smtp_enabled_label">Status</th>
|
||
<td id="smtp-enabled">—</td>
|
||
</tr>
|
||
<tr>
|
||
<th>OXICLOUD_SMTP_HOST</th>
|
||
<td id="smtp-host">—</td>
|
||
</tr>
|
||
<tr>
|
||
<th>OXICLOUD_SMTP_PORT</th>
|
||
<td id="smtp-port">—</td>
|
||
</tr>
|
||
<tr>
|
||
<th>OXICLOUD_SMTP_TLS</th>
|
||
<td id="smtp-tls">—</td>
|
||
</tr>
|
||
<tr>
|
||
<th>OXICLOUD_SMTP_FROM</th>
|
||
<td id="smtp-from">—</td>
|
||
</tr>
|
||
<tr>
|
||
<th>OXICLOUD_SMTP_USER</th>
|
||
<td id="smtp-user-state">—</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<h3 class="mt-14">
|
||
<i class="fas fa-paper-plane"></i> <span data-i18n="admin.smtp_test_title">Send a test email</span>
|
||
</h3>
|
||
<p class="muted" data-i18n="admin.smtp_test_intro">
|
||
Sends a hardcoded diagnostic message to the recipient below and reports the SMTP server's response so you can correlate it with your relay logs.
|
||
</p>
|
||
|
||
<div class="form-group">
|
||
<label for="smtp-test-to" data-i18n="admin.smtp_test_to">Recipient address</label>
|
||
<input
|
||
id="smtp-test-to"
|
||
type="email"
|
||
placeholder="alice@example.com"
|
||
autocomplete="off"
|
||
/>
|
||
</div>
|
||
|
||
<button id="btn-smtp-test" class="btn btn-primary">
|
||
<i class="fas fa-paper-plane"></i> <span data-i18n="admin.smtp_send_test">Send test email</span>
|
||
</button>
|
||
|
||
<div id="smtp-test-result" class="alert" style="display:none; margin-top:14px;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="quota-modal" class="modal-overlay hidden">
|
||
<div class="modal">
|
||
<h3>
|
||
<i class="fas fa-box modal-title-icon"></i> <span data-i18n="admin.quota_modal_title">Update Storage Quota</span>
|
||
</h3>
|
||
<div class="form-group">
|
||
<label><span data-i18n="admin.quota_user_label">User:</span> <strong id="qm-username"></strong></label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="admin.new_quota">New Quota</label>
|
||
<div class="quota-input-row">
|
||
<input
|
||
type="number"
|
||
id="qm-value"
|
||
min="0"
|
||
step="1"
|
||
class="flex-2"
|
||
/>
|
||
<select id="qm-unit" class="select-qm-unit">
|
||
<option value="1073741824">GB</option>
|
||
<option value="1048576">MB</option>
|
||
<option value="1099511627776">TB</option>
|
||
</select>
|
||
</div>
|
||
<small data-i18n="admin.quota_unlimited_hint">Set to 0 for unlimited</small>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button
|
||
class="btn btn-secondary"
|
||
id="btn-close-quota"
|
||
data-i18n="admin.cancel"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button class="btn btn-primary" id="btn-save-quota">
|
||
<i class="fas fa-save"></i> <span data-i18n="admin.save_btn">Save</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="create-user-modal" class="modal-overlay hidden">
|
||
<div class="modal">
|
||
<h3>
|
||
<i class="fas fa-user-plus modal-title-icon"></i> <span data-i18n="admin.create_user_title">Create New User</span>
|
||
</h3>
|
||
<div class="form-group">
|
||
<label><span data-i18n="admin.username_label">Username</span> *</label>
|
||
<input
|
||
type="text"
|
||
id="cu-username"
|
||
placeholder="johndoe"
|
||
data-i18n-placeholder="admin.username_placeholder"
|
||
minlength="3"
|
||
maxlength="32"
|
||
/>
|
||
<small data-i18n="admin.username_hint">3–32 characters</small>
|
||
</div>
|
||
<div class="form-group">
|
||
<label><span data-i18n="admin.password_label">Password</span> *</label>
|
||
<input
|
||
type="password"
|
||
id="cu-password"
|
||
placeholder="Min 8 characters"
|
||
data-i18n-placeholder="admin.password_placeholder"
|
||
minlength="8"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label
|
||
><span data-i18n="admin.email_label">Email</span>
|
||
<small class="small-muted" data-i18n="admin.email_optional">(optional)</small></label
|
||
>
|
||
<input
|
||
type="text"
|
||
id="cu-email"
|
||
placeholder="user@example.com (auto-generated if empty)"
|
||
data-i18n-placeholder="admin.email_placeholder"
|
||
/>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group flex-1">
|
||
<label data-i18n="admin.role_label">Role</label>
|
||
<select id="cu-role" class="select-cu-role">
|
||
<option value="user" data-i18n="admin.role_user">User</option>
|
||
<option value="admin" data-i18n="admin.role_admin">Admin</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group flex-1">
|
||
<label data-i18n="admin.quota_label">Quota</label>
|
||
<div class="quota-row">
|
||
<input
|
||
type="number"
|
||
id="cu-quota-value"
|
||
min="0"
|
||
step="1"
|
||
value="1"
|
||
class="flex-1"
|
||
/>
|
||
<select
|
||
id="cu-quota-unit"
|
||
class="select-cu-quota-unit"
|
||
>
|
||
<option value="1073741824">GB</option>
|
||
<option value="1048576">MB</option>
|
||
<option value="1099511627776">TB</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id="cu-error" class="alert alert-no-margin"></div>
|
||
<div class="modal-actions">
|
||
<button
|
||
class="btn btn-secondary"
|
||
id="btn-close-create-user"
|
||
data-i18n="admin.cancel"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
class="btn btn-primary"
|
||
id="cu-submit"
|
||
>
|
||
<i class="fas fa-user-plus"></i> <span data-i18n="admin.create_user">Create</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="reset-pw-modal" class="modal-overlay hidden">
|
||
<div class="modal">
|
||
<h3>
|
||
<i class="fas fa-key modal-title-icon"></i> <span data-i18n="admin.reset_pw_title">Reset Password</span>
|
||
</h3>
|
||
<div class="form-group">
|
||
<label><span data-i18n="admin.quota_user_label">User:</span> <strong id="rp-username"></strong></label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="admin.new_password_label">New Password</label>
|
||
<input
|
||
type="password"
|
||
id="rp-password"
|
||
placeholder="Min 8 characters"
|
||
minlength="8"
|
||
/>
|
||
</div>
|
||
<div id="rp-error" class="alert alert-no-margin"></div>
|
||
<div class="modal-actions">
|
||
<button
|
||
class="btn btn-secondary"
|
||
id="btn-close-reset-pw"
|
||
data-i18n="admin.cancel"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
class="btn btn-primary"
|
||
id="rp-submit"
|
||
>
|
||
<i class="fas fa-save"></i> <span data-i18n="admin.reset_btn">Reset</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Confirm modal -->
|
||
<div id="confirm-modal" class="modal-overlay hidden">
|
||
<div class="modal modal-confirm">
|
||
<h3 id="confirm-title"><i class="fas fa-exclamation-circle modal-title-icon"></i> <span data-i18n="admin.confirm_action">Confirm Action</span></h3>
|
||
<p id="confirm-message"></p>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" id="confirm-cancel" data-i18n="admin.confirm_no">Cancel</button>
|
||
<button class="btn btn-danger" id="confirm-yes" data-i18n="admin.confirm_yes">Confirm</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script type="module" src="/js/views/admin/admin.js"></script>
|
||
</body>
|
||
</html>
|