Compare commits

...
4 Commits
Author SHA1 Message Date
shuaiplus ff85698edb feat: add Tigris backup provider support with recommendations and localization updates 2026-06-25 21:11:57 +08:00
shuaiplus c3dc53bac1 feat: add Cloudflare R2 support with detailed backup recommendations and localization updates 2026-06-25 19:45:09 +08:00
shuaiplus 1acc31eda0 feat: add Backblaze B2 support with recommendations and styling updates 2026-06-25 18:42:34 +08:00
shuaiplusandClaude c694f1bfce refactor: consolidate security devices UI and remove pending auth requests from settings
- Merge device management and authorized devices sections into a single card in SecurityDevicesPage
- Remove PendingAuthRequestsPanel from SettingsPage and its related props
- Clean up unused auth request prop drilling in AppMainRoutes

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-25 01:49:17 +08:00
14 changed files with 385 additions and 88 deletions
+4 -3
View File
@@ -9,7 +9,8 @@
export const BACKUP_DEFAULT_TIMEZONE = 'UTC';
export const BACKUP_DEFAULT_RETENTION_COUNT = 30;
export const BACKUP_DEFAULT_S3_REGION = 'auto';
export const BACKUP_DEFAULT_REMOTE_PATH = 'nodewarden';
export const BACKUP_DEFAULT_S3_ROOT_PATH = '';
export const BACKUP_DEFAULT_WEBDAV_REMOTE_PATH = 'nodewarden';
export const BACKUP_DEFAULT_INTERVAL_HOURS = 24;
export const BACKUP_DEFAULT_START_TIME = '03:00';
@@ -109,14 +110,14 @@ export function createDefaultBackupDestinationConfig(type: BackupDestinationType
region: BACKUP_DEFAULT_S3_REGION,
accessKeyId: '',
secretAccessKey: '',
rootPath: BACKUP_DEFAULT_REMOTE_PATH,
rootPath: BACKUP_DEFAULT_S3_ROOT_PATH,
};
}
return {
baseUrl: '',
username: '',
password: '',
remotePath: BACKUP_DEFAULT_REMOTE_PATH,
remotePath: BACKUP_DEFAULT_WEBDAV_REMOTE_PATH,
};
}
-5
View File
@@ -275,11 +275,6 @@ export default function AppMainRoutes(props: AppMainRoutesProps) {
onCreateAccountPasskey={props.onCreateAccountPasskey}
onEnableAccountPasskeyDirectUnlock={props.onEnableAccountPasskeyDirectUnlock}
onDeleteAccountPasskey={props.onDeleteAccountPasskey}
pendingAuthRequests={props.pendingAuthRequests}
pendingAuthRequestsLoading={props.pendingAuthRequestsLoading}
onRefreshPendingAuthRequests={props.onRefreshPendingAuthRequests}
onApproveAuthRequest={props.onApproveAuthRequest}
onDenyAuthRequest={props.onDenyAuthRequest}
onLockTimeoutChange={props.onLockTimeoutChange}
onSessionTimeoutActionChange={props.onSessionTimeoutActionChange}
onNotify={props.onNotify}
@@ -91,7 +91,7 @@ export default function SecurityDevicesPage(props: SecurityDevicesPageProps) {
<section className="card">
<div className="section-head">
<div>
<h3 className="flush-title">{t('txt_device_management')}</h3>
<h3 className="flush-title">{t('txt_authorized_devices')}</h3>
<div className="muted-inline section-note">
{t('txt_manage_device_sessions_and_30_day_totp_trusted_sessions')}
</div>
@@ -111,10 +111,6 @@ export default function SecurityDevicesPage(props: SecurityDevicesPageProps) {
</button>
</div>
</div>
</section>
<section className="card">
<h3 className="section-title-flush">{t('txt_authorized_devices')}</h3>
{!!props.error && (
<div className="local-error">
<span>{props.error}</span>
+1 -16
View File
@@ -2,10 +2,9 @@ import { useEffect, useMemo, useState } from 'preact/hooks';
import { Clipboard, KeyRound, RefreshCw, ShieldCheck, ShieldOff, Trash2 } from 'lucide-preact';
import { copyTextToClipboard } from '@/lib/clipboard';
import qrcode from 'qrcode-generator';
import type { AccountPasskeyCredential, AuthRequest, Profile } from '@/lib/types';
import type { AccountPasskeyCredential, Profile } from '@/lib/types';
import { AVAILABLE_LOCALES, getLocale, setLocale, t, type Locale } from '@/lib/i18n';
import ConfirmDialog from '@/components/ConfirmDialog';
import PendingAuthRequestsPanel from '@/components/PendingAuthRequestsPanel';
interface SettingsPageProps {
profile: Profile;
@@ -23,11 +22,6 @@ interface SettingsPageProps {
onCreateAccountPasskey: (name: string, masterPassword: string, directUnlock: boolean) => Promise<AccountPasskeyCredential | null>;
onEnableAccountPasskeyDirectUnlock: (id: string, masterPassword: string) => Promise<void>;
onDeleteAccountPasskey: (id: string, masterPassword: string) => Promise<void>;
pendingAuthRequests: AuthRequest[];
pendingAuthRequestsLoading: boolean;
onRefreshPendingAuthRequests: () => Promise<void>;
onApproveAuthRequest: (request: AuthRequest) => Promise<void>;
onDenyAuthRequest: (request: AuthRequest) => Promise<void>;
onLockTimeoutChange: (minutes: 0 | 1 | 5 | 15 | 30) => void;
onSessionTimeoutActionChange: (action: 'lock' | 'logout') => void;
onNotify?: (type: 'success' | 'error' | 'warning', text: string) => void;
@@ -515,15 +509,6 @@ export default function SettingsPage(props: SettingsPageProps) {
)}
</div>
</section>
<PendingAuthRequestsPanel
pendingAuthRequests={props.pendingAuthRequests}
pendingAuthRequestsLoading={props.pendingAuthRequestsLoading}
onRefreshPendingAuthRequests={props.onRefreshPendingAuthRequests}
onApproveAuthRequest={props.onApproveAuthRequest}
onDenyAuthRequest={props.onDenyAuthRequest}
/>
<section className="settings-module sensitive-actions-module">
<div className="sensitive-actions-grid">
<div className="sensitive-action">
@@ -54,21 +54,18 @@ function renderRecommendedProviderDetails(provider: RecommendedProvider) {
<>
<div className="backup-recommendation-steps">
<div className="backup-recommendation-step">
<strong>1.</strong> {t('txt_backup_recommend_koofr_step_1')}
</div>
<div className="backup-recommendation-step">
<strong>2.</strong> {t('txt_backup_recommend_koofr_step_2_prefix')}{' '}
<strong>1.</strong> {t('txt_backup_recommend_koofr_step_2_prefix')}{' '}
<a href={provider.passwordUrl} target="_blank" rel="noreferrer">{t('txt_backup_recommend_koofr_password_link')}</a>
{t('txt_backup_recommend_koofr_step_2_suffix')}
</div>
<div className="backup-recommendation-step">
<strong>3.</strong> {t('txt_backup_recommend_koofr_step_3')}
<strong>2.</strong> {t('txt_backup_recommend_koofr_step_3')}
</div>
<div className="backup-recommendation-step">
<strong>4.</strong> {t('txt_backup_recommend_koofr_step_4')}
<strong>3.</strong> {t('txt_backup_recommend_koofr_step_4')}
</div>
<div className="backup-recommendation-step">
<strong>5.</strong> {t('txt_backup_recommend_koofr_step_5_prefix')}{' '}
<strong>4.</strong> {t('txt_backup_recommend_koofr_step_5_prefix')}{' '}
<a href={provider.storageUrl} target="_blank" rel="noreferrer">{t('txt_backup_recommend_koofr_storage_link')}</a>
{t('txt_backup_recommend_koofr_step_5_suffix')}
</div>
@@ -98,13 +95,10 @@ function renderRecommendedProviderDetails(provider: RecommendedProvider) {
return (
<div className="backup-recommendation-steps">
<div className="backup-recommendation-step">
<strong>1.</strong> {t('txt_backup_recommend_pcloud_step_1')}
<strong>1.</strong> {t('txt_backup_recommend_pcloud_step_2')}
</div>
<div className="backup-recommendation-step">
<strong>2.</strong> {t('txt_backup_recommend_pcloud_step_2')}
</div>
<div className="backup-recommendation-step">
<strong>3.</strong> {t('txt_backup_recommend_pcloud_step_3')}
<strong>2.</strong> {t('txt_backup_recommend_pcloud_step_3')}
</div>
</div>
);
@@ -112,18 +106,87 @@ function renderRecommendedProviderDetails(provider: RecommendedProvider) {
return (
<div className="backup-recommendation-steps">
<div className="backup-recommendation-step">
<strong>1.</strong> {t('txt_backup_recommend_infinicloud_step_1')}
</div>
<div className="backup-recommendation-step">
<strong>2.</strong> {t('txt_backup_recommend_infinicloud_step_2_prefix')}{' '}
<strong>1.</strong> {t('txt_backup_recommend_infinicloud_step_2_prefix')}{' '}
<a href="https://infini-cloud.net/en/modules/mypage/usage/" target="_blank" rel="noreferrer">My Page</a>
{t('txt_backup_recommend_infinicloud_step_2_suffix')}
</div>
<div className="backup-recommendation-step">
<strong>3.</strong> {t('txt_backup_recommend_infinicloud_step_3')}
<strong>2.</strong> {t('txt_backup_recommend_infinicloud_step_3')}
</div>
<div className="backup-recommendation-step">
<strong>4.</strong> {t('txt_backup_recommend_infinicloud_step_4')}
<strong>3.</strong> {t('txt_backup_recommend_infinicloud_step_4')}
</div>
</div>
);
case 'backblaze-b2':
return (
<div className="backup-recommendation-steps">
<div className="backup-recommendation-step">
<strong>1.</strong> {t('txt_backup_recommend_backblaze_step_2_prefix')}{' '}
<a href={provider.bucketsUrl} target="_blank" rel="noreferrer">Buckets</a>
{t('txt_backup_recommend_backblaze_step_2_suffix')}
</div>
<div className="backup-recommendation-step">
<strong>2.</strong> {t('txt_backup_recommend_backblaze_step_3')}
</div>
<div className="backup-recommendation-step">
<strong>3.</strong> {t('txt_backup_recommend_backblaze_step_4_prefix')}{' '}
<a href={provider.applicationKeysUrl} target="_blank" rel="noreferrer">Application Keys</a>
{t('txt_backup_recommend_backblaze_step_4_suffix')}
</div>
<div className="backup-recommendation-step">
<strong>4.</strong> {t('txt_backup_recommend_backblaze_step_5')}
</div>
<div className="backup-recommendation-step">
<strong>5.</strong> {t('txt_backup_recommend_s3_path_prefix_step')}
</div>
</div>
);
case 'cloudflare-r2':
return (
<div className="backup-recommendation-steps">
<div className="backup-recommendation-step">
<strong>1.</strong> {t('txt_backup_recommend_cloudflare_r2_step_1_prefix')}{' '}
<a href={provider.bucketUrl} target="_blank" rel="noreferrer">{t('txt_backup_recommend_cloudflare_r2_bucket_link')}</a>
{t('txt_backup_recommend_cloudflare_r2_step_1_suffix')}
</div>
<div className="backup-recommendation-step">
<strong>2.</strong> {t('txt_backup_recommend_cloudflare_r2_step_2_prefix')}{' '}
<a href={provider.apiTokenUrl} target="_blank" rel="noreferrer">{t('txt_backup_recommend_cloudflare_r2_api_link')}</a>
{t('txt_backup_recommend_cloudflare_r2_step_2_suffix')}
</div>
<div className="backup-recommendation-step">
<strong>3.</strong> {t('txt_backup_recommend_cloudflare_r2_step_3')}
</div>
<div className="backup-recommendation-step">
<strong>4.</strong> {t('txt_backup_recommend_cloudflare_r2_step_4')}
</div>
<div className="backup-recommendation-step">
<strong>5.</strong> {t('txt_backup_recommend_cloudflare_r2_step_5')}
</div>
</div>
);
case 'tigris':
return (
<div className="backup-recommendation-steps">
<div className="backup-recommendation-step">
<strong>1.</strong> {t('txt_backup_recommend_tigris_step_2_prefix')}{' '}
<a href={provider.bucketUrl} target="_blank" rel="noreferrer">Create Bucket</a>
{t('txt_backup_recommend_tigris_step_2_suffix')}
</div>
<div className="backup-recommendation-step">
<strong>2.</strong> {t('txt_backup_recommend_tigris_step_3_prefix')}{' '}
<a href={provider.accessKeyUrl} target="_blank" rel="noreferrer">{t('txt_backup_recommend_tigris_access_key_link')}</a>
{t('txt_backup_recommend_tigris_step_3_suffix')}
</div>
<div className="backup-recommendation-step">
<strong>3.</strong> {t('txt_backup_recommend_tigris_step_4')}
</div>
<div className="backup-recommendation-step">
<strong>4.</strong> {t('txt_backup_recommend_tigris_step_5')}
</div>
<div className="backup-recommendation-step">
<strong>5.</strong> {t('txt_backup_recommend_s3_path_prefix_step')}
</div>
</div>
);
@@ -147,6 +210,9 @@ export function BackupDestinationDetail(props: BackupDestinationDetailProps) {
<div className="backup-inline-note">
{props.selectedRecommendedProvider.id === 'infinicloud' ? t('txt_backup_recommend_infinicloud_summary')
: props.selectedRecommendedProvider.id === 'koofr' ? t('txt_backup_recommend_koofr_summary')
: props.selectedRecommendedProvider.id === 'backblaze-b2' ? t('txt_backup_recommend_backblaze_summary')
: props.selectedRecommendedProvider.id === 'cloudflare-r2' ? t('txt_backup_recommend_cloudflare_r2_summary')
: props.selectedRecommendedProvider.id === 'tigris' ? t('txt_backup_recommend_tigris_summary')
: t('txt_backup_recommend_pcloud_summary')}
</div>
</div>
@@ -387,7 +453,7 @@ export function BackupDestinationDetail(props: BackupDestinationDetailProps) {
className="input"
value={(props.selectedDestination.destination as WebDavBackupDestination).remotePath}
disabled={props.loadingSettings || props.disableWhileBusy}
placeholder="nodewarden/backups"
placeholder="nodewarden"
onInput={(event) => props.onUpdateDestination((destination) => ({
...destination,
destination: {
@@ -504,7 +570,7 @@ export function BackupDestinationDetail(props: BackupDestinationDetailProps) {
className="input"
value={(props.selectedDestination.destination as S3BackupDestination).rootPath}
disabled={props.loadingSettings || props.disableWhileBusy}
placeholder="nodewarden/backups"
placeholder=""
onInput={(event) => props.onUpdateDestination((destination) => ({
...destination,
destination: {
@@ -1,9 +1,12 @@
import { Download, FileUp } from 'lucide-preact';
import { useEffect, useState } from 'preact/hooks';
import type { RecommendedProvider } from '@/lib/backup-recommendations';
import { hasLinkedStorages } from '@/lib/backup-recommendations';
import { t } from '@/lib/i18n';
import { BackupIncludeAttachmentsField } from './BackupIncludeAttachmentsField';
const MOBILE_RECOMMENDATIONS_QUERY = '(max-width: 760px)';
interface BackupOperationsSidebarProps {
disableWhileBusy: boolean;
exporting: boolean;
@@ -18,7 +21,30 @@ interface BackupOperationsSidebarProps {
onSelectProvider: (providerId: string) => void;
}
function getDefaultRecommendationsOpen() {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
return true;
}
return !window.matchMedia(MOBILE_RECOMMENDATIONS_QUERY).matches;
}
export function BackupOperationsSidebar(props: BackupOperationsSidebarProps) {
const [recommendationsOpen, setRecommendationsOpen] = useState(getDefaultRecommendationsOpen);
const [recommendationsTouched, setRecommendationsTouched] = useState(false);
useEffect(() => {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function' || recommendationsTouched) {
return;
}
const media = window.matchMedia(MOBILE_RECOMMENDATIONS_QUERY);
const syncOpenState = () => setRecommendationsOpen(!media.matches);
syncOpenState();
media.addEventListener('change', syncOpenState);
return () => media.removeEventListener('change', syncOpenState);
}, [recommendationsTouched]);
return (
<aside className="backup-operations-sidebar">
<div className="section-head">
@@ -41,7 +67,14 @@ export function BackupOperationsSidebar(props: BackupOperationsSidebarProps) {
</button>
</div>
<details className="backup-recommendations-disclosure">
<details
className="backup-recommendations-disclosure"
open={recommendationsOpen}
onToggle={(event) => {
setRecommendationsTouched(true);
setRecommendationsOpen((event.currentTarget as HTMLDetailsElement).open);
}}
>
<summary className="backup-recommendations-summary">
<span>
<strong>{t('txt_backup_recommend_title')}</strong>
+47 -2
View File
@@ -4,7 +4,7 @@ export interface RecommendedStorageLink {
}
export interface RecommendedProviderBase {
id: 'infinicloud' | 'koofr' | 'pcloud';
id: 'infinicloud' | 'koofr' | 'pcloud' | 'backblaze-b2' | 'cloudflare-r2' | 'tigris';
name: string;
capacity: string;
protocol: 'webdav' | 's3';
@@ -28,7 +28,25 @@ export interface PcloudProvider extends RecommendedProviderBase {
id: 'pcloud';
}
export type RecommendedProvider = InfinicloudProvider | KoofrProvider | PcloudProvider;
export interface BackblazeB2Provider extends RecommendedProviderBase {
id: 'backblaze-b2';
bucketsUrl: string;
applicationKeysUrl: string;
}
export interface CloudflareR2Provider extends RecommendedProviderBase {
id: 'cloudflare-r2';
bucketUrl: string;
apiTokenUrl: string;
}
export interface TigrisProvider extends RecommendedProviderBase {
id: 'tigris';
bucketUrl: string;
accessKeyUrl: string;
}
export type RecommendedProvider = InfinicloudProvider | KoofrProvider | PcloudProvider | BackblazeB2Provider | CloudflareR2Provider | TigrisProvider;
export const RECOMMENDED_PROVIDERS: RecommendedProvider[] = [
{
@@ -61,6 +79,33 @@ export const RECOMMENDED_PROVIDERS: RecommendedProvider[] = [
signupUrl: 'https://u.pcloud.com/#/register?invite=GITx7ZvEU1N7',
hasAffiliateLink: true,
},
{
id: 'backblaze-b2',
name: 'Backblaze B2',
capacity: '10G',
protocol: 's3',
signupUrl: 'https://secure.backblaze.com/user_signin.htm',
bucketsUrl: 'https://secure.backblaze.com/b2_buckets.htm',
applicationKeysUrl: 'https://secure.backblaze.com/app_keys.htm',
},
{
id: 'cloudflare-r2',
name: 'Cloudflare R2',
capacity: '10G',
protocol: 's3',
signupUrl: 'https://dash.cloudflare.com/?to=/:account/r2/new',
bucketUrl: 'https://dash.cloudflare.com/?to=/:account/r2/new',
apiTokenUrl: 'https://dash.cloudflare.com/?to=/:account/r2/api-tokens/create?type=user',
},
{
id: 'tigris',
name: 'Tigris',
capacity: '5G',
protocol: 's3',
signupUrl: 'https://console.storage.dev/signup',
bucketUrl: 'https://console.storage.dev/createbucket',
accessKeyUrl: 'https://console.storage.dev/createaccesskey',
},
];
export function hasLinkedStorages(provider: RecommendedProvider): provider is KoofrProvider {
+31
View File
@@ -85,6 +85,37 @@ const en: Record<string, string> = {
"txt_backup_recommend_pcloud_step_1": "Register a pCloud account with just your email address.",
"txt_backup_recommend_pcloud_step_2": "Use https://webdav.pcloud.com/ as the WebDAV server URL.",
"txt_backup_recommend_pcloud_step_3": "Use your registration email as the WebDAV username and your account password as the WebDAV password.",
"txt_backup_recommend_backblaze_summary": "S3-compatible object storage with 10 GB free and no credit card required.",
"txt_backup_recommend_backblaze_step_1": "Register or sign in to a Backblaze account.",
"txt_backup_recommend_backblaze_step_2_prefix": "Open",
"txt_backup_recommend_backblaze_step_2_suffix": ", click Create a Bucket, enter only the bucket name, leave the other settings unchanged, and create it.",
"txt_backup_recommend_backblaze_step_3": "After creation, put the displayed Endpoint into S3 Endpoint URL, use the bucket name for Bucket Name, and use the middle segment of the endpoint, such as us-west-004, for Region.",
"txt_backup_recommend_backblaze_step_4_prefix": "Open",
"txt_backup_recommend_backblaze_step_4_suffix": ", click Add a New Application Key, enter any Name of Key, leave the other settings unchanged, and create it.",
"txt_backup_recommend_backblaze_step_5": "Use keyID as the access key and applicationKey as the secret key.",
"txt_backup_recommend_cloudflare_r2_summary": "S3-compatible object storage with 10 GB free, but it requires credit card verification.",
"txt_backup_recommend_cloudflare_r2_bucket_link": "Create bucket page",
"txt_backup_recommend_cloudflare_r2_api_link": "API token page",
"txt_backup_recommend_cloudflare_r2_step_1_prefix": "Open the",
"txt_backup_recommend_cloudflare_r2_step_1_suffix": ", enter only the bucket name, and create it directly.",
"txt_backup_recommend_cloudflare_r2_step_2_prefix": "Open the",
"txt_backup_recommend_cloudflare_r2_step_2_suffix": ", select Object Read & Write for permissions, and create it directly.",
"txt_backup_recommend_cloudflare_r2_step_3": "Ignore the token value after creation. Fill Access Key ID into Access ID, and Secret Access Key into Access Password.",
"txt_backup_recommend_cloudflare_r2_step_4": "Copy the address shown below into S3 Endpoint URL, fill Bucket Name exactly as shown, and leave Region as auto.",
"txt_backup_recommend_cloudflare_r2_step_5": "Set Path Prefix as needed, for example nodewarden, or leave it empty if you do not want a folder prefix.",
"txt_backup_recommend_s3_path_prefix_step": "Set Path Prefix as needed, for example nodewarden, or leave it empty if you do not want a folder prefix.",
"txt_backup_recommend_tigris_summary": "S3-compatible object storage with 5 GB free and no credit card required.",
"txt_backup_recommend_tigris_signup_link": "signup page",
"txt_backup_recommend_tigris_bucket_link": "Create Bucket page",
"txt_backup_recommend_tigris_access_key_link": "Create Access Key page",
"txt_backup_recommend_tigris_step_1_prefix": "Open the",
"txt_backup_recommend_tigris_step_1_suffix": ", sign up, and log in to Tigris.",
"txt_backup_recommend_tigris_step_2_prefix": "Open",
"txt_backup_recommend_tigris_step_2_suffix": ", enter only the bucket name, leave everything else unchanged, and create it.",
"txt_backup_recommend_tigris_step_3_prefix": "Then open the",
"txt_backup_recommend_tigris_step_3_suffix": ", use any name you like, and create it.",
"txt_backup_recommend_tigris_step_4": "Ignore Endpoint URL IAM after creation. Fill the other displayed values into the backup page using the matching field names.",
"txt_backup_recommend_tigris_step_5": "Finally, click Manage Key Permissions and turn on Admin Access, otherwise writing backups will fail.",
"txt_backup_add_destination": "Add Destination",
"txt_backup_schedule_panel_title": "Automatic Schedule",
"txt_backup_schedule_panel_note": "Each destination can keep its own daily backup schedule.",
+31
View File
@@ -85,6 +85,37 @@ const es: Record<string, string> = {
"txt_backup_recommend_pcloud_step_1": "Registre una cuenta pCloud solo con su dirección de correo.",
"txt_backup_recommend_pcloud_step_2": "Use https://webdav.pcloud.com/ como URL del servidor WebDAV.",
"txt_backup_recommend_pcloud_step_3": "Use su correo de registro como nombre de usuario WebDAV y su contraseña de cuenta como contraseña WebDAV.",
"txt_backup_recommend_backblaze_summary": "Almacenamiento de objetos compatible con S3 con 10 GB gratis y sin tarjeta de crédito.",
"txt_backup_recommend_backblaze_step_1": "Registre o inicie sesión en una cuenta de Backblaze.",
"txt_backup_recommend_backblaze_step_2_prefix": "Abra",
"txt_backup_recommend_backblaze_step_2_suffix": ", haga clic en Create a Bucket, introduzca solo el nombre del bucket, deje lo demás sin cambios y créelo.",
"txt_backup_recommend_backblaze_step_3": "Después de crearlo, ponga el Endpoint mostrado en S3 Endpoint URL, use el nombre del bucket en Bucket Name y la parte central del endpoint, como us-west-004, en Region.",
"txt_backup_recommend_backblaze_step_4_prefix": "Abra",
"txt_backup_recommend_backblaze_step_4_suffix": ", haga clic en Add a New Application Key, introduzca cualquier Name of Key, deje lo demás sin cambios y créelo.",
"txt_backup_recommend_backblaze_step_5": "Use keyID como clave de acceso y applicationKey como clave secreta.",
"txt_backup_recommend_cloudflare_r2_summary": "Almacenamiento de objetos compatible con S3 con 10 GB gratis, pero requiere verificación con tarjeta de crédito.",
"txt_backup_recommend_cloudflare_r2_bucket_link": "página para crear bucket",
"txt_backup_recommend_cloudflare_r2_api_link": "página de token API",
"txt_backup_recommend_cloudflare_r2_step_1_prefix": "Abra la",
"txt_backup_recommend_cloudflare_r2_step_1_suffix": ", introduzca solo el nombre del bucket y créelo directamente.",
"txt_backup_recommend_cloudflare_r2_step_2_prefix": "Abra la",
"txt_backup_recommend_cloudflare_r2_step_2_suffix": ", seleccione Object Read & Write en permisos y créelo directamente.",
"txt_backup_recommend_cloudflare_r2_step_3": "Ignore el valor del token después de crearlo. Use Access Key ID como ID de acceso y Secret Access Key como contraseña de acceso.",
"txt_backup_recommend_cloudflare_r2_step_4": "Copie la dirección mostrada abajo en S3 Endpoint URL, rellene Bucket Name tal como aparece y deje Region en auto.",
"txt_backup_recommend_cloudflare_r2_step_5": "Configure Path Prefix si lo necesita, por ejemplo nodewarden, o déjelo vacío si no quiere un prefijo de carpeta.",
"txt_backup_recommend_s3_path_prefix_step": "Configure Path Prefix si lo necesita, por ejemplo nodewarden, o déjelo vacío si no quiere un prefijo de carpeta.",
"txt_backup_recommend_tigris_summary": "Almacenamiento de objetos compatible con S3 con 5 GB gratis y sin tarjeta de crédito.",
"txt_backup_recommend_tigris_signup_link": "página de registro",
"txt_backup_recommend_tigris_bucket_link": "página Create Bucket",
"txt_backup_recommend_tigris_access_key_link": "página Create Access Key",
"txt_backup_recommend_tigris_step_1_prefix": "Abra la",
"txt_backup_recommend_tigris_step_1_suffix": ", regístrese e inicie sesión en Tigris.",
"txt_backup_recommend_tigris_step_2_prefix": "Abra",
"txt_backup_recommend_tigris_step_2_suffix": ", introduzca solo el nombre del bucket, deje todo lo demás sin cambios y créelo.",
"txt_backup_recommend_tigris_step_3_prefix": "Luego abra la",
"txt_backup_recommend_tigris_step_3_suffix": ", use cualquier nombre y créela.",
"txt_backup_recommend_tigris_step_4": "Ignore Endpoint URL IAM después de crearla. Rellene los demás valores mostrados en la página de copia de seguridad usando los nombres correspondientes.",
"txt_backup_recommend_tigris_step_5": "Por último, haga clic en Manage Key Permissions y active Admin Access; de lo contrario, no podrá escribir copias de seguridad.",
"txt_backup_add_destination": "Añadir destino",
"txt_backup_schedule_panel_title": "Programación automática",
"txt_backup_schedule_panel_note": "Cada destino puede mantener su propia programación de copia de seguridad diaria.",
+31
View File
@@ -86,6 +86,37 @@ const ru: Record<string, string> = {
"txt_backup_recommend_pcloud_step_1": "Зарегистрируйте учетную запись pCloud, используя только свой адрес электронной почты.",
"txt_backup_recommend_pcloud_step_2": "Используйте https://webdav.ploud.com/ в качестве URL-адреса сервера WebDAV.",
"txt_backup_recommend_pcloud_step_3": "Используйте свой регистрационный адрес электронной почты в качестве имени пользователя WebDAV и пароль своей учетной записи в качестве пароля WebDAV.",
"txt_backup_recommend_backblaze_summary": "S3-совместимое объектное хранилище с бесплатными 10 ГБ и без кредитной карты.",
"txt_backup_recommend_backblaze_step_1": "Зарегистрируйте учетную запись Backblaze или войдите в нее.",
"txt_backup_recommend_backblaze_step_2_prefix": "Откройте",
"txt_backup_recommend_backblaze_step_2_suffix": ", нажмите Create a Bucket, введите только имя bucket, оставьте остальные настройки без изменений и создайте его.",
"txt_backup_recommend_backblaze_step_3": "После создания вставьте показанный Endpoint в S3 Endpoint URL, имя bucket укажите в Bucket Name, а среднюю часть endpoint, например us-west-004, используйте как Region.",
"txt_backup_recommend_backblaze_step_4_prefix": "Откройте",
"txt_backup_recommend_backblaze_step_4_suffix": ", нажмите Add a New Application Key, введите любое Name of Key, оставьте остальные настройки без изменений и создайте ключ.",
"txt_backup_recommend_backblaze_step_5": "Используйте keyID как ключ доступа, а applicationKey как секретный ключ.",
"txt_backup_recommend_cloudflare_r2_summary": "S3-совместимое объектное хранилище с бесплатными 10 ГБ, но с обязательной проверкой кредитной карты.",
"txt_backup_recommend_cloudflare_r2_bucket_link": "страницу создания bucket",
"txt_backup_recommend_cloudflare_r2_api_link": "страницу API token",
"txt_backup_recommend_cloudflare_r2_step_1_prefix": "Откройте",
"txt_backup_recommend_cloudflare_r2_step_1_suffix": ", введите только имя bucket и сразу создайте его.",
"txt_backup_recommend_cloudflare_r2_step_2_prefix": "Откройте",
"txt_backup_recommend_cloudflare_r2_step_2_suffix": ", выберите Object Read & Write в разрешениях и сразу создайте токен.",
"txt_backup_recommend_cloudflare_r2_step_3": "После создания игнорируйте token value. Введите Access Key ID как ID доступа, а Secret Access Key как пароль доступа.",
"txt_backup_recommend_cloudflare_r2_step_4": "Вставьте показанный ниже адрес в S3 Endpoint URL, заполните Bucket Name как показано и оставьте Region в значении auto.",
"txt_backup_recommend_cloudflare_r2_step_5": "Укажите Path Prefix при необходимости, например nodewarden, или оставьте пустым, если префикс папки не нужен.",
"txt_backup_recommend_s3_path_prefix_step": "Укажите Path Prefix при необходимости, например nodewarden, или оставьте пустым, если префикс папки не нужен.",
"txt_backup_recommend_tigris_summary": "S3-совместимое объектное хранилище с бесплатными 5 ГБ и без кредитной карты.",
"txt_backup_recommend_tigris_signup_link": "страницу регистрации",
"txt_backup_recommend_tigris_bucket_link": "страницу Create Bucket",
"txt_backup_recommend_tigris_access_key_link": "страницу Create Access Key",
"txt_backup_recommend_tigris_step_1_prefix": "Откройте",
"txt_backup_recommend_tigris_step_1_suffix": ", зарегистрируйтесь и войдите в Tigris.",
"txt_backup_recommend_tigris_step_2_prefix": "Откройте",
"txt_backup_recommend_tigris_step_2_suffix": ", введите только имя bucket, ничего больше не меняйте и создайте его.",
"txt_backup_recommend_tigris_step_3_prefix": "Затем откройте",
"txt_backup_recommend_tigris_step_3_suffix": ", введите любое имя и создайте ключ.",
"txt_backup_recommend_tigris_step_4": "После создания игнорируйте Endpoint URL IAM. Остальные показанные значения заполните на странице резервного копирования по совпадающим названиям полей.",
"txt_backup_recommend_tigris_step_5": "В конце нажмите Manage Key Permissions и включите Admin Access, иначе запись резервных копий не будет работать.",
"txt_backup_add_destination": "Добавить пункт назначения",
"txt_backup_schedule_panel_title": "Автоматическое расписание",
"txt_backup_schedule_panel_note": "Каждый пункт назначения может иметь собственный ежедневный график резервного копирования.",
+37 -6
View File
@@ -85,6 +85,37 @@ const zhCN: Record<string, string> = {
"txt_backup_recommend_pcloud_step_1": "先用邮箱注册一个 pCloud 账号。",
"txt_backup_recommend_pcloud_step_2": "WebDAV 地址填写 https://webdav.pcloud.com/ 。",
"txt_backup_recommend_pcloud_step_3": "注册邮箱用作 WebDAV 用户名,注册密码用作 WebDAV 密码。",
"txt_backup_recommend_backblaze_summary": "兼容 S3 的对象存储,免费容量 10 GB,无需信用卡。",
"txt_backup_recommend_backblaze_step_1": "先注册或登录 Backblaze 账号。",
"txt_backup_recommend_backblaze_step_2_prefix": "打开",
"txt_backup_recommend_backblaze_step_2_suffix": ",点击创建一个桶,只输入桶名字,其他地方不修改,然后创建。",
"txt_backup_recommend_backblaze_step_3": "创建后显示的 Endpoint 填到 S3 端点 URL;桶名字填到存储桶名称;区域填 Endpoint 中间那段,例如 us-west-004。",
"txt_backup_recommend_backblaze_step_4_prefix": "打开",
"txt_backup_recommend_backblaze_step_4_suffix": ",点击 Add a New Application Key,随便输入 Name of Key,其他地方不动,然后创建。",
"txt_backup_recommend_backblaze_step_5": "生成结果里的 keyID 填到 访问 IDapplicationKey 填到 访问密码。",
"txt_backup_recommend_cloudflare_r2_summary": "兼容 S3 的对象存储,免费容量 10 GB,需要信用卡认证。",
"txt_backup_recommend_cloudflare_r2_bucket_link": "创建储存桶页面",
"txt_backup_recommend_cloudflare_r2_api_link": "API 创建页面",
"txt_backup_recommend_cloudflare_r2_step_1_prefix": "打开",
"txt_backup_recommend_cloudflare_r2_step_1_suffix": ",只输入存储桶名称,直接创建。",
"txt_backup_recommend_cloudflare_r2_step_2_prefix": "打开",
"txt_backup_recommend_cloudflare_r2_step_2_suffix": ",权限全选“对象读和写”,直接创建。",
"txt_backup_recommend_cloudflare_r2_step_3": "创建后令牌值不用管;Access Key ID 填到 访问 IDSecret Access Key 填到 访问密码。",
"txt_backup_recommend_cloudflare_r2_step_4": "把下面显示的地址填到 S3 端点 URL;存储桶名称如实填写;区域保持 auto 不改。",
"txt_backup_recommend_cloudflare_r2_step_5": "路径前缀按需要填写,例如 nodewarden;不想分目录可以留空。",
"txt_backup_recommend_s3_path_prefix_step": "路径前缀按需要填写,例如 nodewarden;不想分目录可以留空。",
"txt_backup_recommend_tigris_summary": "兼容 S3 的对象存储。免费容量 5 GB,无需信用卡。",
"txt_backup_recommend_tigris_signup_link": "注册页面",
"txt_backup_recommend_tigris_bucket_link": "Create Bucket 页面",
"txt_backup_recommend_tigris_access_key_link": "Create Access Key 页面",
"txt_backup_recommend_tigris_step_1_prefix": "打开",
"txt_backup_recommend_tigris_step_1_suffix": ",注册并登录 Tigris。",
"txt_backup_recommend_tigris_step_2_prefix": "打开",
"txt_backup_recommend_tigris_step_2_suffix": ",只输入桶的名字,其他地方不动,直接创建。",
"txt_backup_recommend_tigris_step_3_prefix": "然后打开",
"txt_backup_recommend_tigris_step_3_suffix": ",名字随意,直接创建。",
"txt_backup_recommend_tigris_step_4": "创建后显示的 Endpoint URL IAM 不用管;其余显示出来的内容按名称填写到备份页面里。",
"txt_backup_recommend_tigris_step_5": "最后点击 Manage Key Permissions,把 Admin Access 打开,否则无法写入。",
"txt_backup_add_destination": "新增地点",
"txt_backup_schedule_panel_title": "自动备份计划",
"txt_backup_schedule_panel_note": "每个备份地点都可以单独配置自己的每日自动备份计划。",
@@ -266,15 +297,15 @@ const zhCN: Record<string, string> = {
"txt_backup_webdav_username": "WebDAV 用户名",
"txt_backup_webdav_password": "WebDAV 密码",
"txt_backup_webdav_path": "远程目录",
"txt_backup_s3_endpoint": "S3 端点",
"txt_backup_s3_addressing_style": "S3 寻址方式",
"txt_backup_s3_endpoint": "S3 端点 URL",
"txt_backup_s3_addressing_style": "寻址方式",
"txt_backup_s3_addressing_path_style": "path-style(默认)",
"txt_backup_s3_addressing_virtual_hosted_style": "virtual-hosted-style",
"txt_backup_s3_bucket": "存储桶",
"txt_backup_s3_bucket": "存储桶名称",
"txt_backup_s3_region": "区域",
"txt_backup_s3_access_key": "访问密钥",
"txt_backup_s3_secret_key": "秘密密钥",
"txt_backup_s3_path": "远程路径",
"txt_backup_s3_access_key": "访问 ID",
"txt_backup_s3_secret_key": "访问密码",
"txt_backup_s3_path": "路径前缀",
"txt_backup_reserved_name": "预留类型名称",
"txt_backup_reserved_notes": "预留备注",
"txt_backup_reserved_notes_placeholder": "给下一个备份地点先留个说明",
+31
View File
@@ -85,6 +85,37 @@ const zhTW: Record<string, string> = {
"txt_backup_recommend_pcloud_step_1": "先用郵箱註冊一個 pCloud 賬號。",
"txt_backup_recommend_pcloud_step_2": "WebDAV 地址填寫 https://webdav.pcloud.com/ 。",
"txt_backup_recommend_pcloud_step_3": "註冊郵箱用作 WebDAV 用戶名,註冊密碼用作 WebDAV 密碼。",
"txt_backup_recommend_backblaze_summary": "兼容 S3 的對象儲存,免費容量 10 GB,無需信用卡。",
"txt_backup_recommend_backblaze_step_1": "先註冊或登入 Backblaze 賬號。",
"txt_backup_recommend_backblaze_step_2_prefix": "打開",
"txt_backup_recommend_backblaze_step_2_suffix": ",點擊創建一個桶,只輸入桶名字,其他地方不修改,然後創建。",
"txt_backup_recommend_backblaze_step_3": "創建後顯示的 Endpoint 填到 S3 端點 URL;桶名字填到儲存桶名稱;區域填 Endpoint 中間那段,例如 us-west-004。",
"txt_backup_recommend_backblaze_step_4_prefix": "打開",
"txt_backup_recommend_backblaze_step_4_suffix": ",點擊 Add a New Application Key,隨便輸入 Name of Key,其他地方不動,然後創建。",
"txt_backup_recommend_backblaze_step_5": "生成結果裡的 keyID 填存取金鑰,applicationKey 填秘密金鑰。",
"txt_backup_recommend_cloudflare_r2_summary": "兼容 S3 的對象儲存,免費容量 10 GB,需要信用卡驗證。",
"txt_backup_recommend_cloudflare_r2_bucket_link": "創建儲存桶頁面",
"txt_backup_recommend_cloudflare_r2_api_link": "API 創建頁面",
"txt_backup_recommend_cloudflare_r2_step_1_prefix": "打開",
"txt_backup_recommend_cloudflare_r2_step_1_suffix": ",只輸入儲存桶名稱,直接創建。",
"txt_backup_recommend_cloudflare_r2_step_2_prefix": "打開",
"txt_backup_recommend_cloudflare_r2_step_2_suffix": ",權限全選「對象讀和寫」,直接創建。",
"txt_backup_recommend_cloudflare_r2_step_3": "創建後令牌值不用管;Access Key ID 填到存取 IDSecret Access Key 填到存取密碼。",
"txt_backup_recommend_cloudflare_r2_step_4": "把下面顯示的地址填到 S3 端點 URL;儲存桶名稱如實填寫;區域保持 auto 不改。",
"txt_backup_recommend_cloudflare_r2_step_5": "路徑前綴按需要填寫,例如 nodewarden;不想分目錄可以留空。",
"txt_backup_recommend_s3_path_prefix_step": "路徑前綴按需要填寫,例如 nodewarden;不想分目錄可以留空。",
"txt_backup_recommend_tigris_summary": "兼容 S3 的對象儲存。免費容量 5 GB,無需信用卡。",
"txt_backup_recommend_tigris_signup_link": "註冊頁面",
"txt_backup_recommend_tigris_bucket_link": "Create Bucket 頁面",
"txt_backup_recommend_tigris_access_key_link": "Create Access Key 頁面",
"txt_backup_recommend_tigris_step_1_prefix": "打開",
"txt_backup_recommend_tigris_step_1_suffix": ",註冊並登入 Tigris。",
"txt_backup_recommend_tigris_step_2_prefix": "打開",
"txt_backup_recommend_tigris_step_2_suffix": ",只輸入桶的名字,其他地方不動,直接創建。",
"txt_backup_recommend_tigris_step_3_prefix": "然後打開",
"txt_backup_recommend_tigris_step_3_suffix": ",名字隨意,直接創建。",
"txt_backup_recommend_tigris_step_4": "創建後顯示的 Endpoint URL IAM 不用管;其餘顯示出來的內容按名稱填寫到備份頁面裡。",
"txt_backup_recommend_tigris_step_5": "最後點擊 Manage Key Permissions,把 Admin Access 打開,否則無法寫入。",
"txt_backup_add_destination": "新增地點",
"txt_backup_schedule_panel_title": "自動備份計劃",
"txt_backup_schedule_panel_note": "每個備份地點都可以單獨配置自己的每日自動備份計劃。",
+9
View File
@@ -351,6 +351,15 @@
color: var(--muted);
}
:root[data-theme='dark'] .backup-recommendation-step a {
color: var(--primary);
}
:root[data-theme='dark'] .backup-recommendation-step a:hover,
:root[data-theme='dark'] .backup-recommendation-step a:focus-visible {
color: var(--primary-strong);
}
:root[data-theme='dark'] .restore-progress-overlay {
background: var(--overlay-strong);
backdrop-filter: blur(8px);
+12
View File
@@ -193,6 +193,18 @@
line-height: 1.5;
}
.backup-recommendation-step a {
color: #1d4ed8;
font-weight: 700;
text-decoration: underline;
text-underline-offset: 2px;
}
.backup-recommendation-step a:hover,
.backup-recommendation-step a:focus-visible {
color: #1742b0;
}
.backup-recommendation-inline-note {
color: #475467;
line-height: 1.5;