mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
feat: added logging system
This commit is contained in:
+8
-2
@@ -22,7 +22,7 @@ import {
|
||||
saveSession,
|
||||
stripProfileSecrets,
|
||||
} from '@/lib/api/auth';
|
||||
import { listAdminInvites, listAdminUsers } from '@/lib/api/admin';
|
||||
import { clearAuditLogs, getAuditLogSettings, listAdminInvites, listAdminUsers, listAuditLogs, saveAuditLogSettings, type AuditLogFilters } from '@/lib/api/admin';
|
||||
import { getDomainRules, saveDomainRules } from '@/lib/api/domains';
|
||||
import { getSends } from '@/lib/api/send';
|
||||
import { getCachedVaultCoreSnapshot, loadVaultCoreSyncSnapshot } from '@/lib/api/vault-sync';
|
||||
@@ -96,6 +96,7 @@ const APP_ROUTE_PATHS = [
|
||||
'/vault/totp',
|
||||
'/sends',
|
||||
'/admin',
|
||||
'/logs',
|
||||
'/security/devices',
|
||||
'/backup',
|
||||
'/settings',
|
||||
@@ -1398,6 +1399,7 @@ export default function App() {
|
||||
if (location === '/vault/totp') return t('txt_verification_code');
|
||||
if (location === '/sends') return t('nav_sends');
|
||||
if (location === '/admin') return t('nav_admin_panel');
|
||||
if (location === '/logs') return t('nav_log_center');
|
||||
if (location === '/security/devices') return t('nav_device_management');
|
||||
if (location === SETTINGS_DOMAIN_RULES_ROUTE) return t('nav_domain_rules');
|
||||
if (location === '/backup') return t('nav_backup_strategy');
|
||||
@@ -1424,7 +1426,7 @@ export default function App() {
|
||||
}, [phase, isImportHashRoute, location, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (phase === 'app' && !isAdminProfile(profile) && location === '/backup' && !profileQuery.isFetching) {
|
||||
if (phase === 'app' && !isAdminProfile(profile) && (location === '/backup' || location === '/logs') && !profileQuery.isFetching) {
|
||||
navigate('/vault');
|
||||
}
|
||||
}, [phase, profile?.role, profileQuery.isFetching, location, navigate]);
|
||||
@@ -1527,6 +1529,10 @@ export default function App() {
|
||||
onToggleUserStatus: adminActions.toggleUserStatus,
|
||||
onDeleteUser: adminActions.deleteUser,
|
||||
onRevokeInvite: adminActions.revokeInvite,
|
||||
onLoadAuditLogs: (filters: AuditLogFilters) => listAuditLogs(authedFetch, filters),
|
||||
onLoadAuditLogSettings: () => getAuditLogSettings(authedFetch),
|
||||
onSaveAuditLogSettings: (settings) => saveAuditLogSettings(authedFetch, settings),
|
||||
onClearAuditLogs: () => clearAuditLogs(authedFetch),
|
||||
onExportBackup: backupActions.exportBackup,
|
||||
onImportBackup: backupActions.importBackup,
|
||||
onImportBackupAllowingChecksumMismatch: backupActions.importBackupAllowingChecksumMismatch,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ArrowUpDown, Check, ChevronDown, Clock3, Cloud, Folder as FolderIcon, Globe2, KeyRound, Lock, LogOut, MonitorSmartphone, Send as SendIcon, Settings as SettingsIcon, ShieldUser, SlidersHorizontal, Users } from 'lucide-preact';
|
||||
import { ArrowUpDown, Check, ChevronDown, Clock3, Cloud, FileClock, Folder as FolderIcon, Globe2, KeyRound, Lock, LogOut, MonitorSmartphone, Send as SendIcon, Settings as SettingsIcon, ShieldUser, SlidersHorizontal, Users } from 'lucide-preact';
|
||||
import type { ComponentChildren } from 'preact';
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { Link } from 'wouter';
|
||||
@@ -48,11 +48,13 @@ function isAdminProfile(profile: Profile | null): boolean {
|
||||
|
||||
export default function AppAuthenticatedShell(props: AppAuthenticatedShellProps) {
|
||||
const routeAnimationKey = props.isImportRoute ? props.importRoute : props.location;
|
||||
const isDomainRulesRoute = props.location === '/settings/domain-rules';
|
||||
const isLogRoute = props.location === '/logs';
|
||||
const isAdmin = isAdminProfile(props.profile);
|
||||
const vaultActive = props.location === '/vault' || props.location === '/vault/totp';
|
||||
const settingsActive = props.location === props.settingsAccountRoute || props.location === '/settings/domain-rules';
|
||||
const dataActive = props.location === '/backup' || props.isImportRoute;
|
||||
const managementActive = props.location === '/admin' || props.location === '/security/devices';
|
||||
const managementActive = props.location === '/admin' || props.location === '/security/devices' || props.location === '/logs';
|
||||
const [navLayoutMode, setNavLayoutMode] = useState<NavLayoutMode>(readNavLayoutMode);
|
||||
const [navLayoutPickerOpen, setNavLayoutPickerOpen] = useState(false);
|
||||
const navLayoutPickerRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -173,6 +175,7 @@ export default function AppAuthenticatedShell(props: AppAuthenticatedShellProps)
|
||||
{isAdmin && renderSideLink('/backup', props.location === '/backup', <Cloud size={16} />, t('nav_backup_strategy'))}
|
||||
{renderSideLink(props.importRoute, props.isImportRoute, <ArrowUpDown size={16} />, t('nav_import_export'))}
|
||||
{isAdmin && renderSideLink('/admin', props.location === '/admin', <Users size={16} />, t('nav_admin_panel'))}
|
||||
{isAdmin && renderSideLink('/logs', props.location === '/logs', <FileClock size={16} />, t('nav_log_center'))}
|
||||
{renderSideLink('/security/devices', props.location === '/security/devices', <MonitorSmartphone size={16} />, t('nav_device_management'))}
|
||||
</>
|
||||
);
|
||||
@@ -217,6 +220,7 @@ export default function AppAuthenticatedShell(props: AppAuthenticatedShellProps)
|
||||
managementActive,
|
||||
<>
|
||||
{isAdmin && renderSubLink('/admin', props.location === '/admin', t('nav_admin_panel'))}
|
||||
{isAdmin && renderSubLink('/logs', props.location === '/logs', t('nav_log_center'))}
|
||||
{renderSubLink('/security/devices', props.location === '/security/devices', t('nav_device_management'))}
|
||||
</>
|
||||
)}
|
||||
@@ -302,7 +306,7 @@ export default function AppAuthenticatedShell(props: AppAuthenticatedShellProps)
|
||||
</div>
|
||||
</aside>
|
||||
<main className="content">
|
||||
<div key={routeAnimationKey} className={`route-stage ${props.location === '/settings/domain-rules' ? 'route-stage-fixed' : ''}`}>
|
||||
<div key={routeAnimationKey} className={`route-stage ${isDomainRulesRoute ? 'route-stage-fixed' : ''} ${isLogRoute ? 'route-stage-log-fixed' : ''}`}>
|
||||
<AppMainRoutes {...props.mainRoutesProps} />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { lazy, Suspense } from 'preact/compat';
|
||||
import { useEffect } from 'preact/hooks';
|
||||
import { Link, Route, Switch } from 'wouter';
|
||||
import { ArrowUpDown, Cloud, Globe2, LogOut, Settings as SettingsIcon, Shield, ShieldUser } from 'lucide-preact';
|
||||
import { ArrowUpDown, Cloud, FileClock, Globe2, LogOut, Settings as SettingsIcon, Shield, ShieldUser } from 'lucide-preact';
|
||||
import type { ImportAttachmentFile, ImportResultSummary } from '@/components/ImportPage';
|
||||
import LoadingState from '@/components/LoadingState';
|
||||
import type { AdminBackupImportResponse, AdminBackupRunResponse, AdminBackupSettings, RemoteBackupBrowserResponse } from '@/lib/api/backup';
|
||||
import type { AuditLogFilters } from '@/lib/api/admin';
|
||||
import type { CiphersImportPayload } from '@/lib/api/vault';
|
||||
import { t } from '@/lib/i18n';
|
||||
import type { AdminInvite, AdminUser, AuthorizedDevice, Cipher, CustomEquivalentDomain, DomainRules, Folder as VaultFolder, Profile, Send, SendDraft, SessionState, VaultDraft } from '@/lib/types';
|
||||
import type { AdminInvite, AdminUser, AuditLogListResult, AuditLogSettings, AuthorizedDevice, Cipher, CustomEquivalentDomain, DomainRules, Folder as VaultFolder, Profile, Send, SendDraft, SessionState, VaultDraft } from '@/lib/types';
|
||||
import type { ExportRequest } from '@/lib/export-formats';
|
||||
|
||||
const VaultPage = lazy(() => import('@/components/VaultPage'));
|
||||
@@ -17,6 +18,7 @@ const SettingsPage = lazy(() => import('@/components/SettingsPage'));
|
||||
const DomainRulesPage = lazy(() => import('@/components/DomainRulesPage'));
|
||||
const SecurityDevicesPage = lazy(() => import('@/components/SecurityDevicesPage'));
|
||||
const AdminPage = lazy(() => import('@/components/AdminPage'));
|
||||
const LogCenterPage = lazy(() => import('@/components/LogCenterPage'));
|
||||
const BackupCenterPage = lazy(() => import('@/components/BackupCenterPage'));
|
||||
const ImportPage = lazy(() => import('@/components/ImportPage'));
|
||||
|
||||
@@ -126,6 +128,10 @@ export interface AppMainRoutesProps {
|
||||
onToggleUserStatus: (userId: string, status: 'active' | 'banned') => Promise<void>;
|
||||
onDeleteUser: (userId: string) => Promise<void>;
|
||||
onRevokeInvite: (code: string) => Promise<void>;
|
||||
onLoadAuditLogs: (filters: AuditLogFilters) => Promise<AuditLogListResult>;
|
||||
onLoadAuditLogSettings: () => Promise<AuditLogSettings>;
|
||||
onSaveAuditLogSettings: (settings: AuditLogSettings) => Promise<AuditLogSettings>;
|
||||
onClearAuditLogs: () => Promise<number>;
|
||||
onExportBackup: (includeAttachments?: boolean) => Promise<void>;
|
||||
onImportBackup: (file: File, replaceExisting?: boolean) => Promise<AdminBackupImportResponse>;
|
||||
onImportBackupAllowingChecksumMismatch: (file: File, replaceExisting?: boolean) => Promise<AdminBackupImportResponse>;
|
||||
@@ -289,6 +295,12 @@ export default function AppMainRoutes(props: AppMainRoutesProps) {
|
||||
<span>{t('nav_admin_panel')}</span>
|
||||
</Link>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<Link href="/logs" className="mobile-settings-link">
|
||||
<FileClock size={18} />
|
||||
<span>{t('nav_log_center')}</span>
|
||||
</Link>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<Link href="/backup" className="mobile-settings-link">
|
||||
<Cloud size={18} />
|
||||
@@ -380,6 +392,23 @@ export default function AppMainRoutes(props: AppMainRoutesProps) {
|
||||
</Suspense>
|
||||
</div>
|
||||
</Route>
|
||||
<Route path="/logs">
|
||||
{isAdmin ? (
|
||||
<div className="stack">
|
||||
<Suspense fallback={<RouteContentFallback />}>
|
||||
<LogCenterPage
|
||||
onLoadLogs={props.onLoadAuditLogs}
|
||||
onLoadSettings={props.onLoadAuditLogSettings}
|
||||
onSaveSettings={props.onSaveAuditLogSettings}
|
||||
onClearLogs={props.onClearAuditLogs}
|
||||
onNotify={props.onNotify}
|
||||
mobileLayout={props.mobileLayout}
|
||||
onMobileBack={() => props.onNavigate(props.settingsHomeRoute)}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
) : null}
|
||||
</Route>
|
||||
{importRoutePaths.map((path) => (
|
||||
<Route key={path} path={path}>
|
||||
{renderImportPageRoute()}
|
||||
|
||||
@@ -0,0 +1,578 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks';
|
||||
import { ChevronLeft, ChevronRight, Database, RefreshCw, Save, Search, Server, Settings2, ShieldAlert, Smartphone, Trash2, UserRound } from 'lucide-preact';
|
||||
import LoadingState from '@/components/LoadingState';
|
||||
import type { AuditLogFilters } from '@/lib/api/admin';
|
||||
import { t } from '@/lib/i18n';
|
||||
import type { AuditLogCategory, AuditLogEntry, AuditLogLevel, AuditLogListResult, AuditLogSettings } from '@/lib/types';
|
||||
|
||||
interface LogCenterPageProps {
|
||||
onLoadLogs: (filters: AuditLogFilters) => Promise<AuditLogListResult>;
|
||||
onLoadSettings: () => Promise<AuditLogSettings>;
|
||||
onSaveSettings: (settings: AuditLogSettings) => Promise<AuditLogSettings>;
|
||||
onClearLogs: () => Promise<number>;
|
||||
onNotify: (type: 'success' | 'error' | 'warning', text: string) => void;
|
||||
mobileLayout?: boolean;
|
||||
onMobileBack?: () => void;
|
||||
}
|
||||
|
||||
type TimeRange = '24h' | '7d' | '30d' | 'all';
|
||||
type FilterCategory = AuditLogCategory | 'all';
|
||||
type FilterLevel = AuditLogLevel | 'all';
|
||||
type RetentionMode = 'days' | 'entries';
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
const CATEGORY_OPTIONS: Array<{ value: FilterCategory; labelKey: string }> = [
|
||||
{ value: 'all', labelKey: 'txt_all_logs' },
|
||||
{ value: 'auth', labelKey: 'txt_log_category_auth' },
|
||||
{ value: 'security', labelKey: 'txt_log_category_security' },
|
||||
{ value: 'device', labelKey: 'txt_log_category_device' },
|
||||
{ value: 'data', labelKey: 'txt_log_category_data' },
|
||||
{ value: 'system', labelKey: 'txt_log_category_system' },
|
||||
];
|
||||
const LEVEL_OPTIONS: Array<{ value: FilterLevel; labelKey: string }> = [
|
||||
{ value: 'all', labelKey: 'txt_all_levels' },
|
||||
{ value: 'info', labelKey: 'txt_log_level_info' },
|
||||
{ value: 'warn', labelKey: 'txt_log_level_warn' },
|
||||
{ value: 'error', labelKey: 'txt_log_level_error' },
|
||||
{ value: 'security', labelKey: 'txt_log_level_security' },
|
||||
];
|
||||
const RANGE_OPTIONS: Array<{ value: TimeRange; labelKey: string }> = [
|
||||
{ value: '24h', labelKey: 'txt_last_24_hours' },
|
||||
{ value: '7d', labelKey: 'txt_last_7_days' },
|
||||
{ value: '30d', labelKey: 'txt_last_30_days' },
|
||||
{ value: 'all', labelKey: 'txt_all_time' },
|
||||
];
|
||||
const RETENTION_OPTIONS: Array<{ value: string; labelKey: string }> = [
|
||||
{ value: '7', labelKey: 'txt_log_retention_7d' },
|
||||
{ value: '30', labelKey: 'txt_log_retention_30d' },
|
||||
{ value: '90', labelKey: 'txt_log_retention_90d' },
|
||||
{ value: '180', labelKey: 'txt_log_retention_180d' },
|
||||
{ value: '365', labelKey: 'txt_log_retention_365d' },
|
||||
{ value: '0', labelKey: 'txt_log_retention_forever' },
|
||||
];
|
||||
const MAX_ENTRY_OPTIONS: Array<{ value: string; labelKey: string }> = [
|
||||
{ value: '1000', labelKey: 'txt_log_max_1000' },
|
||||
{ value: '5000', labelKey: 'txt_log_max_5000' },
|
||||
{ value: '10000', labelKey: 'txt_log_max_10000' },
|
||||
{ value: '50000', labelKey: 'txt_log_max_50000' },
|
||||
{ value: '0', labelKey: 'txt_log_max_unlimited' },
|
||||
];
|
||||
|
||||
function parseMetadata(log: AuditLogEntry): Record<string, unknown> {
|
||||
if (!log.metadata) return {};
|
||||
try {
|
||||
const parsed = JSON.parse(log.metadata);
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
|
||||
} catch {
|
||||
return { raw: log.metadata };
|
||||
}
|
||||
}
|
||||
|
||||
function inferCategory(log: AuditLogEntry, metadata: Record<string, unknown>): AuditLogCategory {
|
||||
if (log.category === 'auth' || log.category === 'security' || log.category === 'device' || log.category === 'data' || log.category === 'system') {
|
||||
return log.category;
|
||||
}
|
||||
const category = metadata.category;
|
||||
if (category === 'auth' || category === 'security' || category === 'device' || category === 'data' || category === 'system') {
|
||||
return category;
|
||||
}
|
||||
if (log.action.startsWith('auth.')) return 'auth';
|
||||
if (log.action.startsWith('device.')) return 'device';
|
||||
if (log.action.startsWith('admin.backup.')) return 'data';
|
||||
if (log.action.startsWith('account.') || log.action.startsWith('user.password.') || log.action.startsWith('user.register.') || log.action.startsWith('admin.user.')) return 'security';
|
||||
return 'system';
|
||||
}
|
||||
|
||||
function inferLevel(log: AuditLogEntry, metadata: Record<string, unknown>): AuditLogLevel {
|
||||
if (log.level === 'info' || log.level === 'warn' || log.level === 'error' || log.level === 'security') {
|
||||
return log.level;
|
||||
}
|
||||
const level = metadata.level;
|
||||
if (level === 'info' || level === 'warn' || level === 'error' || level === 'security') return level;
|
||||
if (log.action.includes('.failed') || log.action.includes('.error')) return 'error';
|
||||
if (log.action.includes('password') || log.action.includes('totp') || log.action.includes('delete') || log.action.includes('ban')) return 'security';
|
||||
return 'info';
|
||||
}
|
||||
|
||||
function humanizeIdentifier(value: string): string {
|
||||
return value
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
||||
.split('.')
|
||||
.flatMap((part) => part.split('_'))
|
||||
.filter(Boolean)
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join(' / ');
|
||||
}
|
||||
|
||||
function keyFor(prefix: string, value: string): string {
|
||||
return `${prefix}${value.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/[^A-Za-z0-9]+/g, '_').toLowerCase()}`;
|
||||
}
|
||||
|
||||
function translatedOrHumanized(key: string, fallback: string): string {
|
||||
const translated = t(key);
|
||||
return translated === key ? humanizeIdentifier(fallback) : translated;
|
||||
}
|
||||
|
||||
function formatAction(action: string): string {
|
||||
if (action.startsWith('auth.refresh.failed.')) {
|
||||
const reason = formatReason(action.slice('auth.refresh.failed.'.length));
|
||||
return t('txt_log_action_auth_refresh_failed', { reason });
|
||||
}
|
||||
return translatedOrHumanized(keyFor('txt_log_action_', action), action);
|
||||
}
|
||||
|
||||
function formatMetaKey(key: string): string {
|
||||
return translatedOrHumanized(keyFor('txt_log_meta_', key), key);
|
||||
}
|
||||
|
||||
function formatReason(reason: string): string {
|
||||
return translatedOrHumanized(keyFor('txt_log_reason_', reason), reason);
|
||||
}
|
||||
|
||||
function formatTime(value: string): string {
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
||||
}
|
||||
|
||||
function formatMetaValue(value: unknown): string {
|
||||
if (value === null || value === undefined || value === '') return t('txt_dash');
|
||||
if (typeof value === 'boolean') return value ? t('txt_yes') : t('txt_no');
|
||||
if (typeof value === 'string') return value;
|
||||
if (typeof value === 'number') return String(value);
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function formatMetaValueForKey(key: string, value: unknown): string {
|
||||
if (key === 'reason' && typeof value === 'string') return formatReason(value);
|
||||
if (key === 'trigger' && typeof value === 'string') {
|
||||
return translatedOrHumanized(keyFor('txt_log_trigger_', value), value);
|
||||
}
|
||||
if (key === 'type' && typeof value === 'string') {
|
||||
return translatedOrHumanized(keyFor('txt_log_target_type_', value), value);
|
||||
}
|
||||
return formatMetaValue(value);
|
||||
}
|
||||
|
||||
function iconForCategory(category: AuditLogCategory) {
|
||||
if (category === 'auth') return <ShieldAlert size={16} />;
|
||||
if (category === 'security') return <UserRound size={16} />;
|
||||
if (category === 'device') return <Smartphone size={16} />;
|
||||
if (category === 'data') return <Database size={16} />;
|
||||
return <Server size={16} />;
|
||||
}
|
||||
|
||||
function buildRange(range: TimeRange): { from?: string; to?: string } {
|
||||
if (range === 'all') return {};
|
||||
const now = Date.now();
|
||||
const hours = range === '24h' ? 24 : range === '7d' ? 24 * 7 : 24 * 30;
|
||||
return {
|
||||
from: new Date(now - hours * 60 * 60 * 1000).toISOString(),
|
||||
to: new Date(now).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function inferRetentionMode(settings: AuditLogSettings): RetentionMode {
|
||||
return settings.retentionDays === null && settings.maxEntries !== null ? 'entries' : 'days';
|
||||
}
|
||||
|
||||
export default function LogCenterPage(props: LogCenterPageProps) {
|
||||
const [logs, setLogs] = useState<AuditLogEntry[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [hasMore, setHasMore] = useState(false);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [search, setSearch] = useState('');
|
||||
const [category, setCategory] = useState<FilterCategory>('all');
|
||||
const [level, setLevel] = useState<FilterLevel>('all');
|
||||
const [range, setRange] = useState<TimeRange>('7d');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [settingsLoading, setSettingsLoading] = useState(false);
|
||||
const [settingsSaving, setSettingsSaving] = useState(false);
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [clearConfirmOpen, setClearConfirmOpen] = useState(false);
|
||||
const [retentionMode, setRetentionMode] = useState<RetentionMode>('days');
|
||||
const [settings, setSettings] = useState<AuditLogSettings>({ retentionDays: 90, maxEntries: null });
|
||||
const [error, setError] = useState('');
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [mobileDetailOpen, setMobileDetailOpen] = useState(false);
|
||||
|
||||
const selectedLog = useMemo(() => logs.find((log) => log.id === selectedId) || logs[0] || null, [logs, selectedId]);
|
||||
const selectedMetadata = useMemo(() => selectedLog ? parseMetadata(selectedLog) : {}, [selectedLog]);
|
||||
const selectedCategory = selectedLog ? inferCategory(selectedLog, selectedMetadata) : 'system';
|
||||
const selectedLevel = selectedLog ? inferLevel(selectedLog, selectedMetadata) : 'info';
|
||||
const page = Math.floor(offset / PAGE_SIZE) + 1;
|
||||
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
||||
|
||||
const load = useCallback(async (nextOffset = offset) => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const rangeFilter = buildRange(range);
|
||||
const result = await props.onLoadLogs({
|
||||
limit: PAGE_SIZE,
|
||||
offset: nextOffset,
|
||||
category,
|
||||
level,
|
||||
q: search,
|
||||
...rangeFilter,
|
||||
});
|
||||
setLogs(result.logs);
|
||||
setTotal(result.total);
|
||||
setHasMore(result.hasMore);
|
||||
setOffset(result.offset);
|
||||
setSelectedId((current) => current && result.logs.some((log) => log.id === current) ? current : result.logs[0]?.id || null);
|
||||
setMobileDetailOpen(false);
|
||||
} catch {
|
||||
setError(t('txt_load_logs_failed'));
|
||||
props.onNotify('error', t('txt_load_logs_failed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [category, level, offset, props, range, search]);
|
||||
|
||||
useEffect(() => {
|
||||
void load(0);
|
||||
}, [category, level, range]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setSettingsLoading(true);
|
||||
props.onLoadSettings()
|
||||
.then((next) => {
|
||||
if (!cancelled) {
|
||||
setSettings(next);
|
||||
setRetentionMode(inferRetentionMode(next));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) props.onNotify('error', t('txt_load_log_settings_failed'));
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setSettingsLoading(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
function submitFilters(event: Event): void {
|
||||
event.preventDefault();
|
||||
void load(0);
|
||||
}
|
||||
|
||||
async function saveSettings(): Promise<void> {
|
||||
setSettingsSaving(true);
|
||||
try {
|
||||
const next = await props.onSaveSettings(settings);
|
||||
setSettings(next);
|
||||
setRetentionMode(inferRetentionMode(next));
|
||||
setSettingsOpen(false);
|
||||
setClearConfirmOpen(false);
|
||||
props.onNotify('success', t('txt_log_settings_saved'));
|
||||
void load(0);
|
||||
} catch {
|
||||
props.onNotify('error', t('txt_log_settings_save_failed'));
|
||||
} finally {
|
||||
setSettingsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function clearLogs(): Promise<void> {
|
||||
setSettingsSaving(true);
|
||||
try {
|
||||
await props.onClearLogs();
|
||||
setLogs([]);
|
||||
setTotal(0);
|
||||
setHasMore(false);
|
||||
setOffset(0);
|
||||
setSelectedId(null);
|
||||
setMobileDetailOpen(false);
|
||||
setClearConfirmOpen(false);
|
||||
setSettingsOpen(false);
|
||||
props.onNotify('success', t('txt_logs_cleared'));
|
||||
} catch {
|
||||
props.onNotify('error', t('txt_clear_logs_failed'));
|
||||
} finally {
|
||||
setSettingsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
function selectRetentionMode(nextMode: RetentionMode): void {
|
||||
setRetentionMode(nextMode);
|
||||
setSettings((current) => nextMode === 'days'
|
||||
? { retentionDays: current.retentionDays ?? 90, maxEntries: null }
|
||||
: { retentionDays: null, maxEntries: current.maxEntries ?? 10_000 });
|
||||
}
|
||||
|
||||
const visibleMetaEntries = selectedLog
|
||||
? Object.entries(selectedMetadata).filter(([key]) => key !== 'category' && key !== 'level')
|
||||
: [];
|
||||
|
||||
function selectLog(logId: string): void {
|
||||
setSelectedId(logId);
|
||||
setSettingsOpen(false);
|
||||
setClearConfirmOpen(false);
|
||||
setMobileDetailOpen(true);
|
||||
}
|
||||
|
||||
function handleMobileBack(): void {
|
||||
if (mobileDetailOpen) {
|
||||
setMobileDetailOpen(false);
|
||||
return;
|
||||
}
|
||||
props.onMobileBack?.();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`log-center-page ${mobileDetailOpen ? 'log-mobile-detail-open' : ''}`}>
|
||||
{props.mobileLayout && (
|
||||
<div className="log-mobile-subhead">
|
||||
<button type="button" className="btn btn-secondary small mobile-settings-back" onClick={handleMobileBack}>
|
||||
<ChevronLeft size={14} className="btn-icon" />
|
||||
{t('txt_back')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-secondary log-mobile-settings-trigger ${settingsOpen ? 'active' : ''}`}
|
||||
aria-label={t('txt_log_settings')}
|
||||
title={t('txt_log_settings')}
|
||||
aria-expanded={settingsOpen}
|
||||
onClick={() => {
|
||||
setSettingsOpen((open) => !open);
|
||||
setClearConfirmOpen(false);
|
||||
}}
|
||||
>
|
||||
<Settings2 size={18} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<section className="card log-center-toolbar">
|
||||
<form className="log-filter-form" onSubmit={submitFilters}>
|
||||
<label className="field log-search-field">
|
||||
<span>{t('txt_search')}</span>
|
||||
<div className="input-action-wrap">
|
||||
<Search size={15} className="input-leading-icon" />
|
||||
<input
|
||||
className="input log-search-input"
|
||||
value={search}
|
||||
placeholder={t('txt_log_search_placeholder')}
|
||||
onInput={(event) => setSearch((event.currentTarget as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>{t('txt_log_category')}</span>
|
||||
<select className="input" value={category} onChange={(event) => setCategory((event.currentTarget as HTMLSelectElement).value as FilterCategory)}>
|
||||
{CATEGORY_OPTIONS.map((option) => <option key={option.value} value={option.value}>{t(option.labelKey)}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>{t('txt_log_level')}</span>
|
||||
<select className="input" value={level} onChange={(event) => setLevel((event.currentTarget as HTMLSelectElement).value as FilterLevel)}>
|
||||
{LEVEL_OPTIONS.map((option) => <option key={option.value} value={option.value}>{t(option.labelKey)}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>{t('txt_time_range')}</span>
|
||||
<select className="input" value={range} onChange={(event) => setRange((event.currentTarget as HTMLSelectElement).value as TimeRange)}>
|
||||
{RANGE_OPTIONS.map((option) => <option key={option.value} value={option.value}>{t(option.labelKey)}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<div className="actions log-filter-actions">
|
||||
<button type="button" className="btn btn-secondary" disabled={loading} onClick={() => void load(offset)}>
|
||||
<RefreshCw size={14} className="btn-icon" />
|
||||
{t('txt_refresh')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-secondary ${settingsOpen ? 'active' : ''}`}
|
||||
aria-expanded={settingsOpen}
|
||||
onClick={() => {
|
||||
setSettingsOpen((open) => !open);
|
||||
setClearConfirmOpen(false);
|
||||
}}
|
||||
>
|
||||
<Settings2 size={14} className="btn-icon" />
|
||||
{t('txt_log_settings')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{settingsOpen && (
|
||||
<div className="log-settings-popover">
|
||||
<div className="section-head log-settings-popover-head">
|
||||
<h3>{t('txt_log_retention_settings')}</h3>
|
||||
</div>
|
||||
<div className="log-settings-mode" role="group" aria-label={t('txt_log_retention_mode')}>
|
||||
<button
|
||||
type="button"
|
||||
className={`log-mode-option ${retentionMode === 'days' ? 'active' : ''}`}
|
||||
disabled={settingsLoading || settingsSaving}
|
||||
onClick={() => selectRetentionMode('days')}
|
||||
>
|
||||
{t('txt_log_retention_mode_days')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`log-mode-option ${retentionMode === 'entries' ? 'active' : ''}`}
|
||||
disabled={settingsLoading || settingsSaving}
|
||||
onClick={() => selectRetentionMode('entries')}
|
||||
>
|
||||
{t('txt_log_retention_mode_entries')}
|
||||
</button>
|
||||
</div>
|
||||
{retentionMode === 'days' ? (
|
||||
<div className="log-settings-retention-block">
|
||||
<label className="log-settings-label" htmlFor="log-retention-days-select">{t('txt_log_retention_days')}</label>
|
||||
<div className="log-settings-retention-row">
|
||||
<select
|
||||
id="log-retention-days-select"
|
||||
className="input"
|
||||
value={String(settings.retentionDays ?? 0)}
|
||||
disabled={settingsLoading || settingsSaving}
|
||||
onChange={(event) => setSettings({
|
||||
retentionDays: Number((event.currentTarget as HTMLSelectElement).value) || null,
|
||||
maxEntries: null,
|
||||
})}
|
||||
>
|
||||
{RETENTION_OPTIONS.map((option) => <option key={option.value} value={option.value}>{t(option.labelKey)}</option>)}
|
||||
</select>
|
||||
<button type="button" className="btn btn-primary log-settings-save-btn" disabled={settingsLoading || settingsSaving} onClick={() => void saveSettings()}>
|
||||
<Save size={14} className="btn-icon" />
|
||||
{t('txt_save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="log-settings-retention-block">
|
||||
<label className="log-settings-label" htmlFor="log-max-entries-select">{t('txt_log_max_entries')}</label>
|
||||
<div className="log-settings-retention-row">
|
||||
<select
|
||||
id="log-max-entries-select"
|
||||
className="input"
|
||||
value={String(settings.maxEntries ?? 0)}
|
||||
disabled={settingsLoading || settingsSaving}
|
||||
onChange={(event) => setSettings({
|
||||
retentionDays: null,
|
||||
maxEntries: Number((event.currentTarget as HTMLSelectElement).value) || null,
|
||||
})}
|
||||
>
|
||||
{MAX_ENTRY_OPTIONS.map((option) => <option key={option.value} value={option.value}>{t(option.labelKey)}</option>)}
|
||||
</select>
|
||||
<button type="button" className="btn btn-primary log-settings-save-btn" disabled={settingsLoading || settingsSaving} onClick={() => void saveSettings()}>
|
||||
<Save size={14} className="btn-icon" />
|
||||
{t('txt_save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="log-settings-danger">
|
||||
{clearConfirmOpen ? (
|
||||
<>
|
||||
<p>{t('txt_clear_logs_confirm')}</p>
|
||||
<div className="actions log-clear-confirm-actions">
|
||||
<button type="button" className="btn btn-secondary" disabled={settingsSaving} onClick={() => setClearConfirmOpen(false)}>
|
||||
{t('txt_cancel')}
|
||||
</button>
|
||||
<button type="button" className="btn btn-danger" disabled={settingsSaving} onClick={() => void clearLogs()}>
|
||||
<Trash2 size={14} className="btn-icon" />
|
||||
{t('txt_clear_all_logs')}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<button type="button" className="btn btn-danger ghost-danger" disabled={settingsLoading || settingsSaving} onClick={() => setClearConfirmOpen(true)}>
|
||||
<Trash2 size={14} className="btn-icon" />
|
||||
{t('txt_clear_all_logs')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<div className="log-center-grid">
|
||||
<section className="card log-list-panel">
|
||||
<div className="section-head">
|
||||
<h3>{t('txt_audit_events')}</h3>
|
||||
<span className="muted-inline">{page} / {totalPages}</span>
|
||||
</div>
|
||||
<div className="log-list">
|
||||
{logs.map((log) => {
|
||||
const metadata = parseMetadata(log);
|
||||
const logCategory = inferCategory(log, metadata);
|
||||
const logLevel = inferLevel(log, metadata);
|
||||
return (
|
||||
<button
|
||||
key={log.id}
|
||||
type="button"
|
||||
className={`log-row ${selectedLog?.id === log.id ? 'active' : ''}`}
|
||||
onClick={() => selectLog(log.id)}
|
||||
>
|
||||
<span className={`log-row-icon log-category-${logCategory}`}>{iconForCategory(logCategory)}</span>
|
||||
<span className="log-row-main">
|
||||
<strong>{formatAction(log.action)}</strong>
|
||||
<small>{formatTime(log.createdAt)}</small>
|
||||
</span>
|
||||
<span className={`log-level-pill log-level-${logLevel}`}>{t(`txt_log_level_${logLevel}`)}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{loading && !logs.length && <LoadingState lines={5} compact />}
|
||||
{!loading && !logs.length && <div className="empty empty-comfortable">{t('txt_no_logs_found')}</div>}
|
||||
{!!error && <div className="local-error">{error}</div>}
|
||||
</div>
|
||||
<div className="actions log-pagination">
|
||||
<button type="button" className="btn btn-secondary small" disabled={loading || offset <= 0} onClick={() => void load(Math.max(0, offset - PAGE_SIZE))}>
|
||||
<ChevronLeft size={14} className="btn-icon" />
|
||||
{t('txt_prev')}
|
||||
</button>
|
||||
<span className="log-pagination-count">
|
||||
{Math.min(offset + logs.length, total)} / {total}
|
||||
</span>
|
||||
<button type="button" className="btn btn-secondary small" disabled={loading || !hasMore} onClick={() => void load(offset + PAGE_SIZE)}>
|
||||
{t('txt_next')}
|
||||
<ChevronRight size={14} className="btn-icon" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="card log-detail-panel">
|
||||
{selectedLog ? (
|
||||
<>
|
||||
<div className="section-head log-detail-head">
|
||||
<div>
|
||||
<h3>{formatAction(selectedLog.action)}</h3>
|
||||
<p className="muted-inline">{selectedLog.action}</p>
|
||||
</div>
|
||||
<span className={`log-level-pill log-level-${selectedLevel}`}>{t(`txt_log_level_${selectedLevel}`)}</span>
|
||||
</div>
|
||||
<div className="log-detail-meta">
|
||||
<div><span>{t('txt_time')}</span><strong>{formatTime(selectedLog.createdAt)}</strong></div>
|
||||
<div><span>{t('txt_log_category')}</span><strong>{t(`txt_log_category_${selectedCategory}`)}</strong></div>
|
||||
<div><span>{t('txt_actor')}</span><strong>{selectedLog.actorEmail || selectedLog.actorUserId || t('txt_dash')}</strong></div>
|
||||
<div><span>{t('txt_target')}</span><strong>{selectedLog.targetUserEmail || String(selectedMetadata.targetEmail || '') || selectedLog.targetId || selectedLog.targetType || t('txt_dash')}</strong></div>
|
||||
</div>
|
||||
<div className="log-detail-json">
|
||||
<h4>{t('txt_metadata')}</h4>
|
||||
{visibleMetaEntries.length ? (
|
||||
<dl>
|
||||
{visibleMetaEntries.map(([key, value]) => (
|
||||
<div key={key}>
|
||||
<dt>{formatMetaKey(key)}</dt>
|
||||
<dd>{formatMetaValueForKey(key, value)}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
) : (
|
||||
<div className="empty">{t('txt_no_metadata')}</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="empty empty-comfortable">{t('txt_no_logs_found')}</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AdminInvite, AdminUser, ListResponse } from '../types';
|
||||
import type { AdminInvite, AdminUser, AuditLogCategory, AuditLogEntry, AuditLogLevel, AuditLogListResult, AuditLogSettings, ListResponse } from '../types';
|
||||
import { parseJson, type AuthedFetch } from './shared';
|
||||
|
||||
export async function listAdminUsers(authedFetch: AuthedFetch): Promise<AdminUser[]> {
|
||||
@@ -51,3 +51,66 @@ export async function deleteUser(authedFetch: AuthedFetch, userId: string): Prom
|
||||
const resp = await authedFetch(`/api/admin/users/${encodeURIComponent(userId)}`, { method: 'DELETE' });
|
||||
if (!resp.ok) throw new Error('Delete user failed');
|
||||
}
|
||||
|
||||
export interface AuditLogFilters {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
category?: AuditLogCategory | 'all';
|
||||
level?: AuditLogLevel | 'all';
|
||||
q?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
}
|
||||
|
||||
export async function listAuditLogs(authedFetch: AuthedFetch, filters: AuditLogFilters = {}): Promise<AuditLogListResult> {
|
||||
const params = new URLSearchParams();
|
||||
params.set('limit', String(filters.limit || 50));
|
||||
params.set('offset', String(filters.offset || 0));
|
||||
if (filters.category && filters.category !== 'all') params.set('category', filters.category);
|
||||
if (filters.level && filters.level !== 'all') params.set('level', filters.level);
|
||||
if (filters.q?.trim()) params.set('q', filters.q.trim());
|
||||
if (filters.from) params.set('from', filters.from);
|
||||
if (filters.to) params.set('to', filters.to);
|
||||
|
||||
const resp = await authedFetch(`/api/admin/logs?${params.toString()}`);
|
||||
if (!resp.ok) throw new Error('Failed to load audit logs');
|
||||
const body = await parseJson<ListResponse<AuditLogEntry>>(resp);
|
||||
return {
|
||||
logs: body?.data || [],
|
||||
total: body?.total || 0,
|
||||
limit: body?.limit || filters.limit || 50,
|
||||
offset: body?.offset || filters.offset || 0,
|
||||
hasMore: !!body?.hasMore,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAuditLogSettings(authedFetch: AuthedFetch): Promise<AuditLogSettings> {
|
||||
const resp = await authedFetch('/api/admin/logs/settings');
|
||||
if (!resp.ok) throw new Error('Failed to load audit log settings');
|
||||
const body = await parseJson<AuditLogSettings & { object?: string }>(resp);
|
||||
return {
|
||||
retentionDays: body?.retentionDays ?? null,
|
||||
maxEntries: body?.maxEntries ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function saveAuditLogSettings(authedFetch: AuthedFetch, settings: AuditLogSettings): Promise<AuditLogSettings> {
|
||||
const resp = await authedFetch('/api/admin/logs/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
if (!resp.ok) throw new Error('Failed to save audit log settings');
|
||||
const body = await parseJson<AuditLogSettings & { object?: string }>(resp);
|
||||
return {
|
||||
retentionDays: body?.retentionDays ?? null,
|
||||
maxEntries: body?.maxEntries ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function clearAuditLogs(authedFetch: AuthedFetch): Promise<number> {
|
||||
const resp = await authedFetch('/api/admin/logs', { method: 'DELETE' });
|
||||
if (!resp.ok) throw new Error('Failed to clear audit logs');
|
||||
const body = await parseJson<{ deleted?: number }>(resp);
|
||||
return Number(body?.deleted || 0);
|
||||
}
|
||||
|
||||
@@ -1137,6 +1137,15 @@ export function createDemoMainRoutesProps(base: AppMainRoutesProps, notify: Noti
|
||||
)));
|
||||
notify('success', t('txt_invite_revoked'));
|
||||
},
|
||||
onLoadAuditLogSettings: async () => ({ retentionDays: 90, maxEntries: null }),
|
||||
onSaveAuditLogSettings: async (settings) => {
|
||||
notify('success', t('txt_log_settings_saved'));
|
||||
return settings;
|
||||
},
|
||||
onClearAuditLogs: async () => {
|
||||
notify('success', t('txt_logs_cleared'));
|
||||
return 0;
|
||||
},
|
||||
onExportBackup: async () => {
|
||||
notify('success', t('txt_backup_export_success'));
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
const en: Record<string, string> = {
|
||||
"nav_account_settings": "Account Settings",
|
||||
"nav_admin_panel": "Admin Panel",
|
||||
"nav_log_center": "Log Center",
|
||||
"nav_device_management": "Device Management",
|
||||
"nav_my_vault": "My Vault",
|
||||
"nav_vault_items": "Vault",
|
||||
@@ -941,6 +942,190 @@ const en: Record<string, string> = {
|
||||
"txt_nav_layout_grouped_expanded_desc": "Keep all groups expanded",
|
||||
"txt_nav_layout_grouped_smart": "Smart groups",
|
||||
"txt_nav_layout_grouped_smart_desc": "Open active groups as needed",
|
||||
"txt_actor": "Actor",
|
||||
"txt_all_levels": "All levels",
|
||||
"txt_all_logs": "All logs",
|
||||
"txt_all_time": "All time",
|
||||
"txt_audit_events": "Log list",
|
||||
"txt_filter": "Filter",
|
||||
"txt_last_24_hours": "Last 24 hours",
|
||||
"txt_last_7_days": "Last 7 days",
|
||||
"txt_last_30_days": "Last 30 days",
|
||||
"txt_load_logs_failed": "Failed to load logs",
|
||||
"txt_load_log_settings_failed": "Failed to load log settings",
|
||||
"txt_log_category": "Category",
|
||||
"txt_log_category_auth": "Auth & sessions",
|
||||
"txt_log_category_data": "Data operations",
|
||||
"txt_log_category_device": "Devices",
|
||||
"txt_log_category_security": "Account security",
|
||||
"txt_log_category_system": "System",
|
||||
"txt_log_center_description": "Trace sign-ins, refresh failures, device events, security changes, backup actions, and admin operations.",
|
||||
"txt_log_center_title": "Log Center",
|
||||
"txt_log_level": "Level",
|
||||
"txt_log_level_error": "Error",
|
||||
"txt_log_level_info": "Info",
|
||||
"txt_log_level_security": "Security",
|
||||
"txt_log_level_warn": "Warn",
|
||||
"txt_log_action_account_api_key_create": "Create API key",
|
||||
"txt_log_action_account_api_key_rotate": "Rotate API key",
|
||||
"txt_log_action_account_keys_update": "Update account keys",
|
||||
"txt_log_action_account_profile_update": "Update account profile",
|
||||
"txt_log_action_account_totp_disable": "Disable two-step login",
|
||||
"txt_log_action_account_totp_enable": "Enable two-step login",
|
||||
"txt_log_action_account_totp_recover": "Recover two-step login",
|
||||
"txt_log_action_account_verify_devices_update": "Update device verification",
|
||||
"txt_log_action_admin_audit_settings_update": "Update log retention settings",
|
||||
"txt_log_action_admin_backup_export": "Export backup",
|
||||
"txt_log_action_admin_backup_import": "Import backup",
|
||||
"txt_log_action_admin_backup_remote_delete": "Delete remote backup",
|
||||
"txt_log_action_admin_backup_remote_manual": "Manual remote backup succeeded",
|
||||
"txt_log_action_admin_backup_remote_manual_failed": "Manual remote backup failed",
|
||||
"txt_log_action_admin_backup_remote_scheduled": "Scheduled remote backup succeeded",
|
||||
"txt_log_action_admin_backup_remote_scheduled_failed": "Scheduled remote backup failed",
|
||||
"txt_log_action_admin_backup_settings_repair": "Repair backup settings",
|
||||
"txt_log_action_admin_backup_settings_update": "Update backup settings",
|
||||
"txt_log_action_admin_invite_create": "Create invite",
|
||||
"txt_log_action_admin_invite_delete_all": "Clear invites",
|
||||
"txt_log_action_admin_invite_revoke": "Revoke invite",
|
||||
"txt_log_action_admin_user_delete": "Delete user",
|
||||
"txt_log_action_admin_user_status": "Change user status",
|
||||
"txt_log_action_attachment_delete": "Delete attachment",
|
||||
"txt_log_action_auth_login_failed_bad_api_key": "Login failed: bad API key",
|
||||
"txt_log_action_auth_login_failed_bad_password": "Login failed: bad password",
|
||||
"txt_log_action_auth_login_failed_user_inactive": "Login failed: inactive account",
|
||||
"txt_log_action_auth_login_success": "Login succeeded",
|
||||
"txt_log_action_auth_refresh_failed": "Refresh login failed: {reason}",
|
||||
"txt_log_action_cipher_delete_permanent": "Permanently delete vault item",
|
||||
"txt_log_action_cipher_delete_permanent_bulk": "Permanently delete vault items",
|
||||
"txt_log_action_cipher_delete_soft": "Move vault item to trash",
|
||||
"txt_log_action_cipher_delete_soft_bulk": "Move vault items to trash",
|
||||
"txt_log_action_device_deactivate": "Deactivate device",
|
||||
"txt_log_action_device_delete": "Delete device",
|
||||
"txt_log_action_device_delete_all": "Delete all devices",
|
||||
"txt_log_action_device_name_update": "Update device name",
|
||||
"txt_log_action_device_trust_permanent": "Trust device permanently",
|
||||
"txt_log_action_device_trust_revoke": "Revoke device trust",
|
||||
"txt_log_action_device_trust_revoke_batch": "Revoke device trust in bulk",
|
||||
"txt_log_action_folder_delete": "Delete folder",
|
||||
"txt_log_action_folder_delete_bulk": "Delete folders",
|
||||
"txt_log_action_send_auth_remove": "Remove Send authentication",
|
||||
"txt_log_action_send_delete": "Delete Send",
|
||||
"txt_log_action_send_delete_bulk": "Delete Sends",
|
||||
"txt_log_action_send_password_remove": "Remove Send password",
|
||||
"txt_log_action_user_password_change": "Change master password",
|
||||
"txt_log_action_user_register_first_admin": "Register first admin",
|
||||
"txt_log_action_user_register_invite": "Register by invite",
|
||||
"txt_log_meta_attachments": "Attachments",
|
||||
"txt_log_meta_bytes": "Bytes",
|
||||
"txt_log_meta_changed": "Changed fields",
|
||||
"txt_log_meta_checksum_mismatch_accepted": "Accepted checksum mismatch",
|
||||
"txt_log_meta_cipher_id": "Vault item ID",
|
||||
"txt_log_meta_ciphers": "Vault items",
|
||||
"txt_log_meta_compat": "Compatibility",
|
||||
"txt_log_meta_compressed_bytes": "Compressed bytes",
|
||||
"txt_log_meta_count": "Count",
|
||||
"txt_log_meta_deleted": "Deleted count",
|
||||
"txt_log_meta_destination_count": "Destination count",
|
||||
"txt_log_meta_destination_id": "Destination ID",
|
||||
"txt_log_meta_destination_name": "Destination name",
|
||||
"txt_log_meta_destination_type": "Destination type",
|
||||
"txt_log_meta_device_identifier": "Device ID",
|
||||
"txt_log_meta_device_type": "Device type",
|
||||
"txt_log_meta_email": "Email",
|
||||
"txt_log_meta_error": "Error",
|
||||
"txt_log_meta_expires_in_hours": "Expires in hours",
|
||||
"txt_log_meta_file_bytes": "File bytes",
|
||||
"txt_log_meta_file_name": "File name",
|
||||
"txt_log_meta_folder_id": "Folder ID",
|
||||
"txt_log_meta_grant_type": "Login method",
|
||||
"txt_log_meta_includes_attachments": "Includes attachments",
|
||||
"txt_log_meta_ip": "IP address",
|
||||
"txt_log_meta_max_entries": "Entry limit",
|
||||
"txt_log_meta_method": "Request method",
|
||||
"txt_log_meta_path": "Request path",
|
||||
"txt_log_meta_provider": "Provider",
|
||||
"txt_log_meta_prune_error": "Cleanup error",
|
||||
"txt_log_meta_pruned_file_count": "Cleaned files",
|
||||
"txt_log_meta_raw": "Raw data",
|
||||
"txt_log_meta_reason": "Reason",
|
||||
"txt_log_meta_remote_path": "Remote path",
|
||||
"txt_log_meta_removed": "Removed count",
|
||||
"txt_log_meta_removed_devices": "Removed devices",
|
||||
"txt_log_meta_removed_sessions": "Removed sessions",
|
||||
"txt_log_meta_removed_trusted": "Trust removals",
|
||||
"txt_log_meta_replace_existing": "Replace existing data",
|
||||
"txt_log_meta_requested": "Requested count",
|
||||
"txt_log_meta_requested_count": "Requested count",
|
||||
"txt_log_meta_retention_days": "Retention days",
|
||||
"txt_log_meta_scheduled_destination_count": "Scheduled destinations",
|
||||
"txt_log_meta_size": "Size",
|
||||
"txt_log_meta_skipped_attachments": "Skipped attachments",
|
||||
"txt_log_meta_skipped_reason": "Skip reason",
|
||||
"txt_log_meta_status": "Status",
|
||||
"txt_log_meta_target_email": "Target email",
|
||||
"txt_log_meta_trigger": "Trigger",
|
||||
"txt_log_meta_type": "Type",
|
||||
"txt_log_meta_updated": "Updated count",
|
||||
"txt_log_meta_upload_verification_attempts": "Upload verification attempts",
|
||||
"txt_log_meta_user_agent": "Browser/client",
|
||||
"txt_log_meta_users": "Users",
|
||||
"txt_log_meta_verify_devices": "Verify devices",
|
||||
"txt_log_meta_web_session": "Web session",
|
||||
"txt_log_reason_bad_api_key": "Bad API key",
|
||||
"txt_log_reason_bad_password": "Bad password",
|
||||
"txt_log_reason_device_missing": "Device missing",
|
||||
"txt_log_reason_device_session_mismatch": "Device session mismatch",
|
||||
"txt_log_reason_token_not_found_or_expired": "Token missing or expired",
|
||||
"txt_log_reason_user_inactive": "User inactive",
|
||||
"txt_log_reason_user_missing": "User missing",
|
||||
"txt_log_target_type_attachment": "Attachment",
|
||||
"txt_log_target_type_audit_log": "Log",
|
||||
"txt_log_target_type_backup": "Backup",
|
||||
"txt_log_target_type_cipher": "Vault item",
|
||||
"txt_log_target_type_device": "Device",
|
||||
"txt_log_target_type_folder": "Folder",
|
||||
"txt_log_target_type_invite": "Invite",
|
||||
"txt_log_target_type_refresh_token": "Refresh token",
|
||||
"txt_log_target_type_send": "Send",
|
||||
"txt_log_target_type_user": "User",
|
||||
"txt_log_trigger_manual": "Manual",
|
||||
"txt_log_trigger_remote": "Remote",
|
||||
"txt_log_trigger_scheduled": "Scheduled",
|
||||
"txt_log_max_1000": "Up to 1,000 entries",
|
||||
"txt_log_max_5000": "Up to 5,000 entries",
|
||||
"txt_log_max_10000": "Up to 10,000 entries",
|
||||
"txt_log_max_50000": "Up to 50,000 entries",
|
||||
"txt_log_max_entries": "Storage cap",
|
||||
"txt_log_max_unlimited": "Unlimited entries",
|
||||
"txt_log_retention_7d": "Keep 7 days",
|
||||
"txt_log_retention_30d": "Keep 30 days",
|
||||
"txt_log_retention_90d": "Keep 90 days",
|
||||
"txt_log_retention_180d": "Keep 180 days",
|
||||
"txt_log_retention_365d": "Keep 365 days",
|
||||
"txt_log_retention_days": "Retention",
|
||||
"txt_log_retention_forever": "Keep forever",
|
||||
"txt_log_retention_hint": "Automatically trims by age and entry count to reduce D1 storage use.",
|
||||
"txt_log_retention_mode": "Retention mode",
|
||||
"txt_log_retention_mode_days": "By time",
|
||||
"txt_log_retention_mode_entries": "By count",
|
||||
"txt_log_retention_settings": "Log retention",
|
||||
"txt_log_settings": "Settings",
|
||||
"txt_log_settings_save_failed": "Failed to save log settings",
|
||||
"txt_log_settings_saved": "Log settings saved",
|
||||
"txt_log_search_placeholder": "Search action, actor, target, request path, or metadata",
|
||||
"txt_log_total": " total",
|
||||
"txt_log_visible": " visible",
|
||||
"txt_metadata": "Metadata",
|
||||
"txt_no_logs_found": "No logs found",
|
||||
"txt_no_metadata": "No metadata",
|
||||
"txt_clear_all_logs": "Clear logs",
|
||||
"txt_clear_logs_confirm": "Clear all logs? This cannot be undone.",
|
||||
"txt_clear_logs_failed": "Failed to clear logs",
|
||||
"txt_logs_cleared": "Logs cleared",
|
||||
"txt_search": "Search",
|
||||
"txt_target": "Target",
|
||||
"txt_time": "Time",
|
||||
"txt_time_range": "Time range",
|
||||
"txt_remove_domain": "Remove domain"
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
const es: Record<string, string> = {
|
||||
"nav_account_settings": "Configuración de la cuenta",
|
||||
"nav_admin_panel": "Panel de administración",
|
||||
"nav_log_center": "Centro de registros",
|
||||
"nav_device_management": "Gestión de dispositivos",
|
||||
"nav_my_vault": "Mi bóveda",
|
||||
"nav_vault_items": "Bóveda",
|
||||
@@ -941,6 +942,190 @@ const es: Record<string, string> = {
|
||||
"txt_nav_layout_grouped_expanded_desc": "Mantener todos los grupos abiertos",
|
||||
"txt_nav_layout_grouped_smart": "Grupos inteligentes",
|
||||
"txt_nav_layout_grouped_smart_desc": "Abrir grupos activos cuando haga falta",
|
||||
"txt_actor": "Actor",
|
||||
"txt_all_levels": "Todos los niveles",
|
||||
"txt_all_logs": "Todos los registros",
|
||||
"txt_all_time": "Todo el tiempo",
|
||||
"txt_audit_events": "Lista de registros",
|
||||
"txt_filter": "Filtrar",
|
||||
"txt_last_24_hours": "Últimas 24 horas",
|
||||
"txt_last_7_days": "Últimos 7 días",
|
||||
"txt_last_30_days": "Últimos 30 días",
|
||||
"txt_load_logs_failed": "No se pudieron cargar los registros",
|
||||
"txt_load_log_settings_failed": "No se pudo cargar la configuración de registros",
|
||||
"txt_log_category": "Categoría",
|
||||
"txt_log_category_auth": "Acceso y sesiones",
|
||||
"txt_log_category_data": "Operaciones de datos",
|
||||
"txt_log_category_device": "Dispositivos",
|
||||
"txt_log_category_security": "Seguridad de cuenta",
|
||||
"txt_log_category_system": "Sistema",
|
||||
"txt_log_center_description": "Revisa inicios de sesión, fallos de renovación, eventos de dispositivos, cambios de seguridad, copias y acciones de administración.",
|
||||
"txt_log_center_title": "Centro de registros",
|
||||
"txt_log_level": "Nivel",
|
||||
"txt_log_level_error": "Error",
|
||||
"txt_log_level_info": "Info",
|
||||
"txt_log_level_security": "Seguridad",
|
||||
"txt_log_level_warn": "Aviso",
|
||||
"txt_log_action_account_api_key_create": "Create API key",
|
||||
"txt_log_action_account_api_key_rotate": "Rotate API key",
|
||||
"txt_log_action_account_keys_update": "Update account keys",
|
||||
"txt_log_action_account_profile_update": "Update account profile",
|
||||
"txt_log_action_account_totp_disable": "Disable two-step login",
|
||||
"txt_log_action_account_totp_enable": "Enable two-step login",
|
||||
"txt_log_action_account_totp_recover": "Recover two-step login",
|
||||
"txt_log_action_account_verify_devices_update": "Update device verification",
|
||||
"txt_log_action_admin_audit_settings_update": "Update log retention settings",
|
||||
"txt_log_action_admin_backup_export": "Export backup",
|
||||
"txt_log_action_admin_backup_import": "Import backup",
|
||||
"txt_log_action_admin_backup_remote_delete": "Delete remote backup",
|
||||
"txt_log_action_admin_backup_remote_manual": "Manual remote backup succeeded",
|
||||
"txt_log_action_admin_backup_remote_manual_failed": "Manual remote backup failed",
|
||||
"txt_log_action_admin_backup_remote_scheduled": "Scheduled remote backup succeeded",
|
||||
"txt_log_action_admin_backup_remote_scheduled_failed": "Scheduled remote backup failed",
|
||||
"txt_log_action_admin_backup_settings_repair": "Repair backup settings",
|
||||
"txt_log_action_admin_backup_settings_update": "Update backup settings",
|
||||
"txt_log_action_admin_invite_create": "Create invite",
|
||||
"txt_log_action_admin_invite_delete_all": "Clear invites",
|
||||
"txt_log_action_admin_invite_revoke": "Revoke invite",
|
||||
"txt_log_action_admin_user_delete": "Delete user",
|
||||
"txt_log_action_admin_user_status": "Change user status",
|
||||
"txt_log_action_attachment_delete": "Delete attachment",
|
||||
"txt_log_action_auth_login_failed_bad_api_key": "Login failed: bad API key",
|
||||
"txt_log_action_auth_login_failed_bad_password": "Login failed: bad password",
|
||||
"txt_log_action_auth_login_failed_user_inactive": "Login failed: inactive account",
|
||||
"txt_log_action_auth_login_success": "Login succeeded",
|
||||
"txt_log_action_auth_refresh_failed": "Refresh login failed: {reason}",
|
||||
"txt_log_action_cipher_delete_permanent": "Permanently delete vault item",
|
||||
"txt_log_action_cipher_delete_permanent_bulk": "Permanently delete vault items",
|
||||
"txt_log_action_cipher_delete_soft": "Move vault item to trash",
|
||||
"txt_log_action_cipher_delete_soft_bulk": "Move vault items to trash",
|
||||
"txt_log_action_device_deactivate": "Deactivate device",
|
||||
"txt_log_action_device_delete": "Delete device",
|
||||
"txt_log_action_device_delete_all": "Delete all devices",
|
||||
"txt_log_action_device_name_update": "Update device name",
|
||||
"txt_log_action_device_trust_permanent": "Trust device permanently",
|
||||
"txt_log_action_device_trust_revoke": "Revoke device trust",
|
||||
"txt_log_action_device_trust_revoke_batch": "Revoke device trust in bulk",
|
||||
"txt_log_action_folder_delete": "Delete folder",
|
||||
"txt_log_action_folder_delete_bulk": "Delete folders",
|
||||
"txt_log_action_send_auth_remove": "Remove Send authentication",
|
||||
"txt_log_action_send_delete": "Delete Send",
|
||||
"txt_log_action_send_delete_bulk": "Delete Sends",
|
||||
"txt_log_action_send_password_remove": "Remove Send password",
|
||||
"txt_log_action_user_password_change": "Change master password",
|
||||
"txt_log_action_user_register_first_admin": "Register first admin",
|
||||
"txt_log_action_user_register_invite": "Register by invite",
|
||||
"txt_log_meta_attachments": "Attachments",
|
||||
"txt_log_meta_bytes": "Bytes",
|
||||
"txt_log_meta_changed": "Changed fields",
|
||||
"txt_log_meta_checksum_mismatch_accepted": "Accepted checksum mismatch",
|
||||
"txt_log_meta_cipher_id": "Vault item ID",
|
||||
"txt_log_meta_ciphers": "Vault items",
|
||||
"txt_log_meta_compat": "Compatibility",
|
||||
"txt_log_meta_compressed_bytes": "Compressed bytes",
|
||||
"txt_log_meta_count": "Count",
|
||||
"txt_log_meta_deleted": "Deleted count",
|
||||
"txt_log_meta_destination_count": "Destination count",
|
||||
"txt_log_meta_destination_id": "Destination ID",
|
||||
"txt_log_meta_destination_name": "Destination name",
|
||||
"txt_log_meta_destination_type": "Destination type",
|
||||
"txt_log_meta_device_identifier": "Device ID",
|
||||
"txt_log_meta_device_type": "Device type",
|
||||
"txt_log_meta_email": "Email",
|
||||
"txt_log_meta_error": "Error",
|
||||
"txt_log_meta_expires_in_hours": "Expires in hours",
|
||||
"txt_log_meta_file_bytes": "File bytes",
|
||||
"txt_log_meta_file_name": "File name",
|
||||
"txt_log_meta_folder_id": "Folder ID",
|
||||
"txt_log_meta_grant_type": "Login method",
|
||||
"txt_log_meta_includes_attachments": "Includes attachments",
|
||||
"txt_log_meta_ip": "IP address",
|
||||
"txt_log_meta_max_entries": "Entry limit",
|
||||
"txt_log_meta_method": "Request method",
|
||||
"txt_log_meta_path": "Request path",
|
||||
"txt_log_meta_provider": "Provider",
|
||||
"txt_log_meta_prune_error": "Cleanup error",
|
||||
"txt_log_meta_pruned_file_count": "Cleaned files",
|
||||
"txt_log_meta_raw": "Raw data",
|
||||
"txt_log_meta_reason": "Reason",
|
||||
"txt_log_meta_remote_path": "Remote path",
|
||||
"txt_log_meta_removed": "Removed count",
|
||||
"txt_log_meta_removed_devices": "Removed devices",
|
||||
"txt_log_meta_removed_sessions": "Removed sessions",
|
||||
"txt_log_meta_removed_trusted": "Trust removals",
|
||||
"txt_log_meta_replace_existing": "Replace existing data",
|
||||
"txt_log_meta_requested": "Requested count",
|
||||
"txt_log_meta_requested_count": "Requested count",
|
||||
"txt_log_meta_retention_days": "Retention days",
|
||||
"txt_log_meta_scheduled_destination_count": "Scheduled destinations",
|
||||
"txt_log_meta_size": "Size",
|
||||
"txt_log_meta_skipped_attachments": "Skipped attachments",
|
||||
"txt_log_meta_skipped_reason": "Skip reason",
|
||||
"txt_log_meta_status": "Status",
|
||||
"txt_log_meta_target_email": "Target email",
|
||||
"txt_log_meta_trigger": "Trigger",
|
||||
"txt_log_meta_type": "Type",
|
||||
"txt_log_meta_updated": "Updated count",
|
||||
"txt_log_meta_upload_verification_attempts": "Upload verification attempts",
|
||||
"txt_log_meta_user_agent": "Browser/client",
|
||||
"txt_log_meta_users": "Users",
|
||||
"txt_log_meta_verify_devices": "Verify devices",
|
||||
"txt_log_meta_web_session": "Web session",
|
||||
"txt_log_reason_bad_api_key": "Bad API key",
|
||||
"txt_log_reason_bad_password": "Bad password",
|
||||
"txt_log_reason_device_missing": "Device missing",
|
||||
"txt_log_reason_device_session_mismatch": "Device session mismatch",
|
||||
"txt_log_reason_token_not_found_or_expired": "Token missing or expired",
|
||||
"txt_log_reason_user_inactive": "User inactive",
|
||||
"txt_log_reason_user_missing": "User missing",
|
||||
"txt_log_target_type_attachment": "Attachment",
|
||||
"txt_log_target_type_audit_log": "Log",
|
||||
"txt_log_target_type_backup": "Backup",
|
||||
"txt_log_target_type_cipher": "Vault item",
|
||||
"txt_log_target_type_device": "Device",
|
||||
"txt_log_target_type_folder": "Folder",
|
||||
"txt_log_target_type_invite": "Invite",
|
||||
"txt_log_target_type_refresh_token": "Refresh token",
|
||||
"txt_log_target_type_send": "Send",
|
||||
"txt_log_target_type_user": "User",
|
||||
"txt_log_trigger_manual": "Manual",
|
||||
"txt_log_trigger_remote": "Remote",
|
||||
"txt_log_trigger_scheduled": "Scheduled",
|
||||
"txt_log_max_1000": "Hasta 1000 entradas",
|
||||
"txt_log_max_5000": "Hasta 5000 entradas",
|
||||
"txt_log_max_10000": "Hasta 10 000 entradas",
|
||||
"txt_log_max_50000": "Hasta 50 000 entradas",
|
||||
"txt_log_max_entries": "Límite de almacenamiento",
|
||||
"txt_log_max_unlimited": "Entradas ilimitadas",
|
||||
"txt_log_retention_7d": "Conservar 7 días",
|
||||
"txt_log_retention_30d": "Conservar 30 días",
|
||||
"txt_log_retention_90d": "Conservar 90 días",
|
||||
"txt_log_retention_180d": "Conservar 180 días",
|
||||
"txt_log_retention_365d": "Conservar 365 días",
|
||||
"txt_log_retention_days": "Retención",
|
||||
"txt_log_retention_forever": "Conservar siempre",
|
||||
"txt_log_retention_hint": "Recorta automáticamente por antigüedad y cantidad para reducir el uso de D1.",
|
||||
"txt_log_retention_mode": "Modo de retención",
|
||||
"txt_log_retention_mode_days": "Por tiempo",
|
||||
"txt_log_retention_mode_entries": "Por cantidad",
|
||||
"txt_log_retention_settings": "Retención de registros",
|
||||
"txt_log_settings": "Configuración",
|
||||
"txt_log_settings_save_failed": "No se pudo guardar la configuración de registros",
|
||||
"txt_log_settings_saved": "Configuración de registros guardada",
|
||||
"txt_log_search_placeholder": "Buscar acción, actor, destino, ruta o metadatos",
|
||||
"txt_log_total": " total",
|
||||
"txt_log_visible": " visibles",
|
||||
"txt_metadata": "Metadatos",
|
||||
"txt_no_logs_found": "No se encontraron registros",
|
||||
"txt_no_metadata": "Sin metadatos",
|
||||
"txt_clear_all_logs": "Borrar registros",
|
||||
"txt_clear_logs_confirm": "¿Borrar todos los registros? Esta acción no se puede deshacer.",
|
||||
"txt_clear_logs_failed": "No se pudieron borrar los registros",
|
||||
"txt_logs_cleared": "Registros borrados",
|
||||
"txt_search": "Buscar",
|
||||
"txt_target": "Destino",
|
||||
"txt_time": "Hora",
|
||||
"txt_time_range": "Rango de tiempo",
|
||||
"txt_remove_domain": "Quitar dominio"
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ const ru: Record<string, string> = {
|
||||
"txt_backup_destination_detail_note": "",
|
||||
"nav_account_settings": "Настройки учетной записи",
|
||||
"nav_admin_panel": "Панель администратора",
|
||||
"nav_log_center": "Центр журналов",
|
||||
"nav_device_management": "Управление устройствами",
|
||||
"nav_my_vault": "Мое хранилище",
|
||||
"nav_vault_items": "Хранилище",
|
||||
@@ -941,6 +942,190 @@ const ru: Record<string, string> = {
|
||||
"txt_nav_layout_grouped_expanded_desc": "Держать все группы открытыми",
|
||||
"txt_nav_layout_grouped_smart": "Умные группы",
|
||||
"txt_nav_layout_grouped_smart_desc": "Открывать активные группы по необходимости",
|
||||
"txt_actor": "Инициатор",
|
||||
"txt_all_levels": "Все уровни",
|
||||
"txt_all_logs": "Все журналы",
|
||||
"txt_all_time": "Все время",
|
||||
"txt_audit_events": "Список журналов",
|
||||
"txt_filter": "Фильтр",
|
||||
"txt_last_24_hours": "Последние 24 часа",
|
||||
"txt_last_7_days": "Последние 7 дней",
|
||||
"txt_last_30_days": "Последние 30 дней",
|
||||
"txt_load_logs_failed": "Не удалось загрузить журналы",
|
||||
"txt_load_log_settings_failed": "Не удалось загрузить настройки журналов",
|
||||
"txt_log_category": "Категория",
|
||||
"txt_log_category_auth": "Вход и сессии",
|
||||
"txt_log_category_data": "Операции с данными",
|
||||
"txt_log_category_device": "Устройства",
|
||||
"txt_log_category_security": "Безопасность учетной записи",
|
||||
"txt_log_category_system": "Система",
|
||||
"txt_log_center_description": "Просматривайте входы, сбои обновления, события устройств, изменения безопасности, резервные копии и действия администратора.",
|
||||
"txt_log_center_title": "Центр журналов",
|
||||
"txt_log_level": "Уровень",
|
||||
"txt_log_level_error": "Ошибка",
|
||||
"txt_log_level_info": "Инфо",
|
||||
"txt_log_level_security": "Безопасность",
|
||||
"txt_log_level_warn": "Предупреждение",
|
||||
"txt_log_action_account_api_key_create": "Create API key",
|
||||
"txt_log_action_account_api_key_rotate": "Rotate API key",
|
||||
"txt_log_action_account_keys_update": "Update account keys",
|
||||
"txt_log_action_account_profile_update": "Update account profile",
|
||||
"txt_log_action_account_totp_disable": "Disable two-step login",
|
||||
"txt_log_action_account_totp_enable": "Enable two-step login",
|
||||
"txt_log_action_account_totp_recover": "Recover two-step login",
|
||||
"txt_log_action_account_verify_devices_update": "Update device verification",
|
||||
"txt_log_action_admin_audit_settings_update": "Update log retention settings",
|
||||
"txt_log_action_admin_backup_export": "Export backup",
|
||||
"txt_log_action_admin_backup_import": "Import backup",
|
||||
"txt_log_action_admin_backup_remote_delete": "Delete remote backup",
|
||||
"txt_log_action_admin_backup_remote_manual": "Manual remote backup succeeded",
|
||||
"txt_log_action_admin_backup_remote_manual_failed": "Manual remote backup failed",
|
||||
"txt_log_action_admin_backup_remote_scheduled": "Scheduled remote backup succeeded",
|
||||
"txt_log_action_admin_backup_remote_scheduled_failed": "Scheduled remote backup failed",
|
||||
"txt_log_action_admin_backup_settings_repair": "Repair backup settings",
|
||||
"txt_log_action_admin_backup_settings_update": "Update backup settings",
|
||||
"txt_log_action_admin_invite_create": "Create invite",
|
||||
"txt_log_action_admin_invite_delete_all": "Clear invites",
|
||||
"txt_log_action_admin_invite_revoke": "Revoke invite",
|
||||
"txt_log_action_admin_user_delete": "Delete user",
|
||||
"txt_log_action_admin_user_status": "Change user status",
|
||||
"txt_log_action_attachment_delete": "Delete attachment",
|
||||
"txt_log_action_auth_login_failed_bad_api_key": "Login failed: bad API key",
|
||||
"txt_log_action_auth_login_failed_bad_password": "Login failed: bad password",
|
||||
"txt_log_action_auth_login_failed_user_inactive": "Login failed: inactive account",
|
||||
"txt_log_action_auth_login_success": "Login succeeded",
|
||||
"txt_log_action_auth_refresh_failed": "Refresh login failed: {reason}",
|
||||
"txt_log_action_cipher_delete_permanent": "Permanently delete vault item",
|
||||
"txt_log_action_cipher_delete_permanent_bulk": "Permanently delete vault items",
|
||||
"txt_log_action_cipher_delete_soft": "Move vault item to trash",
|
||||
"txt_log_action_cipher_delete_soft_bulk": "Move vault items to trash",
|
||||
"txt_log_action_device_deactivate": "Deactivate device",
|
||||
"txt_log_action_device_delete": "Delete device",
|
||||
"txt_log_action_device_delete_all": "Delete all devices",
|
||||
"txt_log_action_device_name_update": "Update device name",
|
||||
"txt_log_action_device_trust_permanent": "Trust device permanently",
|
||||
"txt_log_action_device_trust_revoke": "Revoke device trust",
|
||||
"txt_log_action_device_trust_revoke_batch": "Revoke device trust in bulk",
|
||||
"txt_log_action_folder_delete": "Delete folder",
|
||||
"txt_log_action_folder_delete_bulk": "Delete folders",
|
||||
"txt_log_action_send_auth_remove": "Remove Send authentication",
|
||||
"txt_log_action_send_delete": "Delete Send",
|
||||
"txt_log_action_send_delete_bulk": "Delete Sends",
|
||||
"txt_log_action_send_password_remove": "Remove Send password",
|
||||
"txt_log_action_user_password_change": "Change master password",
|
||||
"txt_log_action_user_register_first_admin": "Register first admin",
|
||||
"txt_log_action_user_register_invite": "Register by invite",
|
||||
"txt_log_meta_attachments": "Attachments",
|
||||
"txt_log_meta_bytes": "Bytes",
|
||||
"txt_log_meta_changed": "Changed fields",
|
||||
"txt_log_meta_checksum_mismatch_accepted": "Accepted checksum mismatch",
|
||||
"txt_log_meta_cipher_id": "Vault item ID",
|
||||
"txt_log_meta_ciphers": "Vault items",
|
||||
"txt_log_meta_compat": "Compatibility",
|
||||
"txt_log_meta_compressed_bytes": "Compressed bytes",
|
||||
"txt_log_meta_count": "Count",
|
||||
"txt_log_meta_deleted": "Deleted count",
|
||||
"txt_log_meta_destination_count": "Destination count",
|
||||
"txt_log_meta_destination_id": "Destination ID",
|
||||
"txt_log_meta_destination_name": "Destination name",
|
||||
"txt_log_meta_destination_type": "Destination type",
|
||||
"txt_log_meta_device_identifier": "Device ID",
|
||||
"txt_log_meta_device_type": "Device type",
|
||||
"txt_log_meta_email": "Email",
|
||||
"txt_log_meta_error": "Error",
|
||||
"txt_log_meta_expires_in_hours": "Expires in hours",
|
||||
"txt_log_meta_file_bytes": "File bytes",
|
||||
"txt_log_meta_file_name": "File name",
|
||||
"txt_log_meta_folder_id": "Folder ID",
|
||||
"txt_log_meta_grant_type": "Login method",
|
||||
"txt_log_meta_includes_attachments": "Includes attachments",
|
||||
"txt_log_meta_ip": "IP address",
|
||||
"txt_log_meta_max_entries": "Entry limit",
|
||||
"txt_log_meta_method": "Request method",
|
||||
"txt_log_meta_path": "Request path",
|
||||
"txt_log_meta_provider": "Provider",
|
||||
"txt_log_meta_prune_error": "Cleanup error",
|
||||
"txt_log_meta_pruned_file_count": "Cleaned files",
|
||||
"txt_log_meta_raw": "Raw data",
|
||||
"txt_log_meta_reason": "Reason",
|
||||
"txt_log_meta_remote_path": "Remote path",
|
||||
"txt_log_meta_removed": "Removed count",
|
||||
"txt_log_meta_removed_devices": "Removed devices",
|
||||
"txt_log_meta_removed_sessions": "Removed sessions",
|
||||
"txt_log_meta_removed_trusted": "Trust removals",
|
||||
"txt_log_meta_replace_existing": "Replace existing data",
|
||||
"txt_log_meta_requested": "Requested count",
|
||||
"txt_log_meta_requested_count": "Requested count",
|
||||
"txt_log_meta_retention_days": "Retention days",
|
||||
"txt_log_meta_scheduled_destination_count": "Scheduled destinations",
|
||||
"txt_log_meta_size": "Size",
|
||||
"txt_log_meta_skipped_attachments": "Skipped attachments",
|
||||
"txt_log_meta_skipped_reason": "Skip reason",
|
||||
"txt_log_meta_status": "Status",
|
||||
"txt_log_meta_target_email": "Target email",
|
||||
"txt_log_meta_trigger": "Trigger",
|
||||
"txt_log_meta_type": "Type",
|
||||
"txt_log_meta_updated": "Updated count",
|
||||
"txt_log_meta_upload_verification_attempts": "Upload verification attempts",
|
||||
"txt_log_meta_user_agent": "Browser/client",
|
||||
"txt_log_meta_users": "Users",
|
||||
"txt_log_meta_verify_devices": "Verify devices",
|
||||
"txt_log_meta_web_session": "Web session",
|
||||
"txt_log_reason_bad_api_key": "Bad API key",
|
||||
"txt_log_reason_bad_password": "Bad password",
|
||||
"txt_log_reason_device_missing": "Device missing",
|
||||
"txt_log_reason_device_session_mismatch": "Device session mismatch",
|
||||
"txt_log_reason_token_not_found_or_expired": "Token missing or expired",
|
||||
"txt_log_reason_user_inactive": "User inactive",
|
||||
"txt_log_reason_user_missing": "User missing",
|
||||
"txt_log_target_type_attachment": "Attachment",
|
||||
"txt_log_target_type_audit_log": "Log",
|
||||
"txt_log_target_type_backup": "Backup",
|
||||
"txt_log_target_type_cipher": "Vault item",
|
||||
"txt_log_target_type_device": "Device",
|
||||
"txt_log_target_type_folder": "Folder",
|
||||
"txt_log_target_type_invite": "Invite",
|
||||
"txt_log_target_type_refresh_token": "Refresh token",
|
||||
"txt_log_target_type_send": "Send",
|
||||
"txt_log_target_type_user": "User",
|
||||
"txt_log_trigger_manual": "Manual",
|
||||
"txt_log_trigger_remote": "Remote",
|
||||
"txt_log_trigger_scheduled": "Scheduled",
|
||||
"txt_log_max_1000": "До 1 000 записей",
|
||||
"txt_log_max_5000": "До 5 000 записей",
|
||||
"txt_log_max_10000": "До 10 000 записей",
|
||||
"txt_log_max_50000": "До 50 000 записей",
|
||||
"txt_log_max_entries": "Лимит хранения",
|
||||
"txt_log_max_unlimited": "Без ограничения записей",
|
||||
"txt_log_retention_7d": "Хранить 7 дней",
|
||||
"txt_log_retention_30d": "Хранить 30 дней",
|
||||
"txt_log_retention_90d": "Хранить 90 дней",
|
||||
"txt_log_retention_180d": "Хранить 180 дней",
|
||||
"txt_log_retention_365d": "Хранить 365 дней",
|
||||
"txt_log_retention_days": "Срок хранения",
|
||||
"txt_log_retention_forever": "Хранить всегда",
|
||||
"txt_log_retention_hint": "Автоматически обрезает по возрасту и количеству, чтобы уменьшить использование D1.",
|
||||
"txt_log_retention_mode": "Режим хранения",
|
||||
"txt_log_retention_mode_days": "По времени",
|
||||
"txt_log_retention_mode_entries": "По количеству",
|
||||
"txt_log_retention_settings": "Хранение журналов",
|
||||
"txt_log_settings": "Настройки",
|
||||
"txt_log_settings_save_failed": "Не удалось сохранить настройки журналов",
|
||||
"txt_log_settings_saved": "Настройки журналов сохранены",
|
||||
"txt_log_search_placeholder": "Поиск действия, инициатора, цели, пути или метаданных",
|
||||
"txt_log_total": " всего",
|
||||
"txt_log_visible": " показано",
|
||||
"txt_metadata": "Метаданные",
|
||||
"txt_no_logs_found": "Журналы не найдены",
|
||||
"txt_no_metadata": "Нет метаданных",
|
||||
"txt_clear_all_logs": "Очистить журналы",
|
||||
"txt_clear_logs_confirm": "Очистить все журналы? Это действие нельзя отменить.",
|
||||
"txt_clear_logs_failed": "Не удалось очистить журналы",
|
||||
"txt_logs_cleared": "Журналы очищены",
|
||||
"txt_search": "Поиск",
|
||||
"txt_target": "Цель",
|
||||
"txt_time": "Время",
|
||||
"txt_time_range": "Период",
|
||||
"txt_remove_domain": "Удалить домен"
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
const zhCN: Record<string, string> = {
|
||||
"nav_account_settings": "账户设置",
|
||||
"nav_admin_panel": "用户管理",
|
||||
"nav_log_center": "日志中心",
|
||||
"nav_device_management": "设备管理",
|
||||
"nav_my_vault": "我的密码库",
|
||||
"nav_vault_items": "密码库",
|
||||
@@ -941,6 +942,190 @@ const zhCN: Record<string, string> = {
|
||||
"txt_nav_layout_grouped_expanded_desc": "父子菜单全部展开",
|
||||
"txt_nav_layout_grouped_smart": "智能分组",
|
||||
"txt_nav_layout_grouped_smart_desc": "当前相关分组自动展开",
|
||||
"txt_actor": "操作者",
|
||||
"txt_all_levels": "全部级别",
|
||||
"txt_all_logs": "全部日志",
|
||||
"txt_all_time": "全部时间",
|
||||
"txt_audit_events": "日志列表",
|
||||
"txt_filter": "筛选",
|
||||
"txt_last_24_hours": "最近 24 小时",
|
||||
"txt_last_7_days": "最近 7 天",
|
||||
"txt_last_30_days": "最近 30 天",
|
||||
"txt_load_logs_failed": "加载日志失败",
|
||||
"txt_load_log_settings_failed": "加载日志设置失败",
|
||||
"txt_log_category": "分类",
|
||||
"txt_log_category_auth": "登录与会话",
|
||||
"txt_log_category_data": "数据操作",
|
||||
"txt_log_category_device": "设备",
|
||||
"txt_log_category_security": "账户安全",
|
||||
"txt_log_category_system": "系统",
|
||||
"txt_log_center_description": "查看登录、刷新失败、设备事件、安全变更、备份操作和管理员操作。",
|
||||
"txt_log_center_title": "日志中心",
|
||||
"txt_log_level": "级别",
|
||||
"txt_log_level_error": "错误",
|
||||
"txt_log_level_info": "信息",
|
||||
"txt_log_level_security": "安全",
|
||||
"txt_log_level_warn": "警告",
|
||||
"txt_log_action_account_api_key_create": "创建 API 密钥",
|
||||
"txt_log_action_account_api_key_rotate": "轮换 API 密钥",
|
||||
"txt_log_action_account_keys_update": "更新账户密钥",
|
||||
"txt_log_action_account_profile_update": "更新账户资料",
|
||||
"txt_log_action_account_totp_disable": "关闭两步验证",
|
||||
"txt_log_action_account_totp_enable": "开启两步验证",
|
||||
"txt_log_action_account_totp_recover": "恢复两步验证",
|
||||
"txt_log_action_account_verify_devices_update": "更新设备验证设置",
|
||||
"txt_log_action_admin_audit_settings_update": "更新日志保留设置",
|
||||
"txt_log_action_admin_backup_export": "导出备份",
|
||||
"txt_log_action_admin_backup_import": "导入备份",
|
||||
"txt_log_action_admin_backup_remote_delete": "删除远程备份",
|
||||
"txt_log_action_admin_backup_remote_manual": "手动远程备份成功",
|
||||
"txt_log_action_admin_backup_remote_manual_failed": "手动远程备份失败",
|
||||
"txt_log_action_admin_backup_remote_scheduled": "计划远程备份成功",
|
||||
"txt_log_action_admin_backup_remote_scheduled_failed": "计划远程备份失败",
|
||||
"txt_log_action_admin_backup_settings_repair": "修复备份设置",
|
||||
"txt_log_action_admin_backup_settings_update": "更新备份设置",
|
||||
"txt_log_action_admin_invite_create": "创建邀请",
|
||||
"txt_log_action_admin_invite_delete_all": "清空邀请",
|
||||
"txt_log_action_admin_invite_revoke": "撤销邀请",
|
||||
"txt_log_action_admin_user_delete": "删除用户",
|
||||
"txt_log_action_admin_user_status": "修改用户状态",
|
||||
"txt_log_action_attachment_delete": "删除附件",
|
||||
"txt_log_action_auth_login_failed_bad_api_key": "API 密钥错误登录失败",
|
||||
"txt_log_action_auth_login_failed_bad_password": "密码错误登录失败",
|
||||
"txt_log_action_auth_login_failed_user_inactive": "账号停用登录失败",
|
||||
"txt_log_action_auth_login_success": "登录成功",
|
||||
"txt_log_action_auth_refresh_failed": "刷新登录失败:{reason}",
|
||||
"txt_log_action_cipher_delete_permanent": "永久删除密码项",
|
||||
"txt_log_action_cipher_delete_permanent_bulk": "批量永久删除密码项",
|
||||
"txt_log_action_cipher_delete_soft": "删除到回收站",
|
||||
"txt_log_action_cipher_delete_soft_bulk": "批量删除到回收站",
|
||||
"txt_log_action_device_deactivate": "停用设备",
|
||||
"txt_log_action_device_delete": "删除设备",
|
||||
"txt_log_action_device_delete_all": "删除全部设备",
|
||||
"txt_log_action_device_name_update": "修改设备名称",
|
||||
"txt_log_action_device_trust_permanent": "永久信任设备",
|
||||
"txt_log_action_device_trust_revoke": "撤销设备信任",
|
||||
"txt_log_action_device_trust_revoke_batch": "批量撤销设备信任",
|
||||
"txt_log_action_folder_delete": "删除文件夹",
|
||||
"txt_log_action_folder_delete_bulk": "批量删除文件夹",
|
||||
"txt_log_action_send_auth_remove": "移除 Send 验证",
|
||||
"txt_log_action_send_delete": "删除 Send",
|
||||
"txt_log_action_send_delete_bulk": "批量删除 Send",
|
||||
"txt_log_action_send_password_remove": "移除 Send 密码",
|
||||
"txt_log_action_user_password_change": "修改主密码",
|
||||
"txt_log_action_user_register_first_admin": "注册首个管理员",
|
||||
"txt_log_action_user_register_invite": "通过邀请注册",
|
||||
"txt_log_meta_attachments": "附件数",
|
||||
"txt_log_meta_bytes": "字节数",
|
||||
"txt_log_meta_changed": "变更项",
|
||||
"txt_log_meta_checksum_mismatch_accepted": "已接受校验不一致",
|
||||
"txt_log_meta_cipher_id": "密码项 ID",
|
||||
"txt_log_meta_ciphers": "密码项数量",
|
||||
"txt_log_meta_compat": "兼容信息",
|
||||
"txt_log_meta_compressed_bytes": "压缩后字节数",
|
||||
"txt_log_meta_count": "数量",
|
||||
"txt_log_meta_deleted": "已删除数量",
|
||||
"txt_log_meta_destination_count": "备份目标数量",
|
||||
"txt_log_meta_destination_id": "备份目标 ID",
|
||||
"txt_log_meta_destination_name": "备份目标名称",
|
||||
"txt_log_meta_destination_type": "备份目标类型",
|
||||
"txt_log_meta_device_identifier": "设备 ID",
|
||||
"txt_log_meta_device_type": "设备类型",
|
||||
"txt_log_meta_email": "邮箱",
|
||||
"txt_log_meta_error": "错误",
|
||||
"txt_log_meta_expires_in_hours": "过期小时数",
|
||||
"txt_log_meta_file_bytes": "文件字节数",
|
||||
"txt_log_meta_file_name": "文件名",
|
||||
"txt_log_meta_folder_id": "文件夹 ID",
|
||||
"txt_log_meta_grant_type": "登录方式",
|
||||
"txt_log_meta_includes_attachments": "包含附件",
|
||||
"txt_log_meta_ip": "IP 地址",
|
||||
"txt_log_meta_max_entries": "条数上限",
|
||||
"txt_log_meta_method": "请求方法",
|
||||
"txt_log_meta_path": "请求路径",
|
||||
"txt_log_meta_provider": "服务提供方",
|
||||
"txt_log_meta_prune_error": "清理错误",
|
||||
"txt_log_meta_pruned_file_count": "已清理文件数",
|
||||
"txt_log_meta_raw": "原始数据",
|
||||
"txt_log_meta_reason": "原因",
|
||||
"txt_log_meta_remote_path": "远程路径",
|
||||
"txt_log_meta_removed": "已移除数量",
|
||||
"txt_log_meta_removed_devices": "已移除设备数",
|
||||
"txt_log_meta_removed_sessions": "已移除会话数",
|
||||
"txt_log_meta_removed_trusted": "已撤销信任数",
|
||||
"txt_log_meta_replace_existing": "覆盖现有数据",
|
||||
"txt_log_meta_requested": "请求数量",
|
||||
"txt_log_meta_requested_count": "请求数量",
|
||||
"txt_log_meta_retention_days": "保留天数",
|
||||
"txt_log_meta_scheduled_destination_count": "已计划备份目标数",
|
||||
"txt_log_meta_size": "大小",
|
||||
"txt_log_meta_skipped_attachments": "跳过附件数",
|
||||
"txt_log_meta_skipped_reason": "跳过原因",
|
||||
"txt_log_meta_status": "状态",
|
||||
"txt_log_meta_target_email": "目标邮箱",
|
||||
"txt_log_meta_trigger": "触发方式",
|
||||
"txt_log_meta_type": "类型",
|
||||
"txt_log_meta_updated": "已更新数量",
|
||||
"txt_log_meta_upload_verification_attempts": "上传校验次数",
|
||||
"txt_log_meta_user_agent": "浏览器/客户端",
|
||||
"txt_log_meta_users": "用户数量",
|
||||
"txt_log_meta_verify_devices": "验证设备",
|
||||
"txt_log_meta_web_session": "网页会话",
|
||||
"txt_log_reason_bad_api_key": "API 密钥错误",
|
||||
"txt_log_reason_bad_password": "密码错误",
|
||||
"txt_log_reason_device_missing": "设备不存在",
|
||||
"txt_log_reason_device_session_mismatch": "设备会话不匹配",
|
||||
"txt_log_reason_token_not_found_or_expired": "令牌不存在或已过期",
|
||||
"txt_log_reason_user_inactive": "用户未启用",
|
||||
"txt_log_reason_user_missing": "用户不存在",
|
||||
"txt_log_target_type_attachment": "附件",
|
||||
"txt_log_target_type_audit_log": "日志",
|
||||
"txt_log_target_type_backup": "备份",
|
||||
"txt_log_target_type_cipher": "密码项",
|
||||
"txt_log_target_type_device": "设备",
|
||||
"txt_log_target_type_folder": "文件夹",
|
||||
"txt_log_target_type_invite": "邀请",
|
||||
"txt_log_target_type_refresh_token": "刷新令牌",
|
||||
"txt_log_target_type_send": "Send",
|
||||
"txt_log_target_type_user": "用户",
|
||||
"txt_log_trigger_manual": "手动",
|
||||
"txt_log_trigger_remote": "远程",
|
||||
"txt_log_trigger_scheduled": "计划任务",
|
||||
"txt_log_max_1000": "最多 1,000 条",
|
||||
"txt_log_max_5000": "最多 5,000 条",
|
||||
"txt_log_max_10000": "最多 10,000 条",
|
||||
"txt_log_max_50000": "最多 50,000 条",
|
||||
"txt_log_max_entries": "容量上限",
|
||||
"txt_log_max_unlimited": "不限制条数",
|
||||
"txt_log_retention_7d": "保留 7 天",
|
||||
"txt_log_retention_30d": "保留 30 天",
|
||||
"txt_log_retention_90d": "保留 90 天",
|
||||
"txt_log_retention_180d": "保留 180 天",
|
||||
"txt_log_retention_365d": "保留 365 天",
|
||||
"txt_log_retention_days": "保留时间",
|
||||
"txt_log_retention_forever": "永久保留",
|
||||
"txt_log_retention_hint": "按时间和最大条数自动收缩,减少 D1 存储占用。",
|
||||
"txt_log_retention_mode": "保留方式",
|
||||
"txt_log_retention_mode_days": "按时间",
|
||||
"txt_log_retention_mode_entries": "按条数",
|
||||
"txt_log_retention_settings": "日志保留",
|
||||
"txt_log_settings": "设置",
|
||||
"txt_log_settings_save_failed": "保存日志设置失败",
|
||||
"txt_log_settings_saved": "日志设置已保存",
|
||||
"txt_log_search_placeholder": "搜索动作、操作者、目标、请求路径或元数据",
|
||||
"txt_log_total": " 条总数",
|
||||
"txt_log_visible": " 条显示",
|
||||
"txt_metadata": "元数据",
|
||||
"txt_no_logs_found": "没有找到日志",
|
||||
"txt_no_metadata": "没有元数据",
|
||||
"txt_clear_all_logs": "清空日志",
|
||||
"txt_clear_logs_confirm": "确定清空全部日志吗?此操作无法撤销。",
|
||||
"txt_clear_logs_failed": "清空日志失败",
|
||||
"txt_logs_cleared": "日志已清空",
|
||||
"txt_search": "搜索",
|
||||
"txt_target": "目标",
|
||||
"txt_time": "时间",
|
||||
"txt_time_range": "时间范围",
|
||||
"txt_remove_domain": "移除域名"
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
const zhTW: Record<string, string> = {
|
||||
"nav_account_settings": "賬戶設置",
|
||||
"nav_admin_panel": "用戶管理",
|
||||
"nav_log_center": "日誌中心",
|
||||
"nav_device_management": "設備管理",
|
||||
"nav_my_vault": "我的密碼庫",
|
||||
"nav_vault_items": "密碼庫",
|
||||
@@ -941,6 +942,190 @@ const zhTW: Record<string, string> = {
|
||||
"txt_nav_layout_grouped_expanded_desc": "父子選單全部展開",
|
||||
"txt_nav_layout_grouped_smart": "智能分組",
|
||||
"txt_nav_layout_grouped_smart_desc": "目前相關分組自動展開",
|
||||
"txt_actor": "操作者",
|
||||
"txt_all_levels": "全部級別",
|
||||
"txt_all_logs": "全部日誌",
|
||||
"txt_all_time": "全部時間",
|
||||
"txt_audit_events": "日誌列表",
|
||||
"txt_filter": "篩選",
|
||||
"txt_last_24_hours": "最近 24 小時",
|
||||
"txt_last_7_days": "最近 7 天",
|
||||
"txt_last_30_days": "最近 30 天",
|
||||
"txt_load_logs_failed": "載入日誌失敗",
|
||||
"txt_load_log_settings_failed": "載入日誌設定失敗",
|
||||
"txt_log_category": "分類",
|
||||
"txt_log_category_auth": "登入與會話",
|
||||
"txt_log_category_data": "資料操作",
|
||||
"txt_log_category_device": "設備",
|
||||
"txt_log_category_security": "賬戶安全",
|
||||
"txt_log_category_system": "系統",
|
||||
"txt_log_center_description": "查看登入、刷新失敗、設備事件、安全變更、備份操作和管理員操作。",
|
||||
"txt_log_center_title": "日誌中心",
|
||||
"txt_log_level": "級別",
|
||||
"txt_log_level_error": "錯誤",
|
||||
"txt_log_level_info": "資訊",
|
||||
"txt_log_level_security": "安全",
|
||||
"txt_log_level_warn": "警告",
|
||||
"txt_log_action_account_api_key_create": "建立 API 金鑰",
|
||||
"txt_log_action_account_api_key_rotate": "輪換 API 金鑰",
|
||||
"txt_log_action_account_keys_update": "更新帳戶金鑰",
|
||||
"txt_log_action_account_profile_update": "更新帳戶資料",
|
||||
"txt_log_action_account_totp_disable": "關閉兩步驟登入",
|
||||
"txt_log_action_account_totp_enable": "開啟兩步驟登入",
|
||||
"txt_log_action_account_totp_recover": "復原兩步驟登入",
|
||||
"txt_log_action_account_verify_devices_update": "更新裝置驗證設定",
|
||||
"txt_log_action_admin_audit_settings_update": "更新日誌保留設定",
|
||||
"txt_log_action_admin_backup_export": "匯出備份",
|
||||
"txt_log_action_admin_backup_import": "匯入備份",
|
||||
"txt_log_action_admin_backup_remote_delete": "刪除遠端備份",
|
||||
"txt_log_action_admin_backup_remote_manual": "手動遠端備份成功",
|
||||
"txt_log_action_admin_backup_remote_manual_failed": "手動遠端備份失敗",
|
||||
"txt_log_action_admin_backup_remote_scheduled": "排程遠端備份成功",
|
||||
"txt_log_action_admin_backup_remote_scheduled_failed": "排程遠端備份失敗",
|
||||
"txt_log_action_admin_backup_settings_repair": "修復備份設定",
|
||||
"txt_log_action_admin_backup_settings_update": "更新備份設定",
|
||||
"txt_log_action_admin_invite_create": "建立邀請",
|
||||
"txt_log_action_admin_invite_delete_all": "清空邀請",
|
||||
"txt_log_action_admin_invite_revoke": "撤銷邀請",
|
||||
"txt_log_action_admin_user_delete": "刪除使用者",
|
||||
"txt_log_action_admin_user_status": "修改使用者狀態",
|
||||
"txt_log_action_attachment_delete": "刪除附件",
|
||||
"txt_log_action_auth_login_failed_bad_api_key": "API 金鑰錯誤登入失敗",
|
||||
"txt_log_action_auth_login_failed_bad_password": "密碼錯誤登入失敗",
|
||||
"txt_log_action_auth_login_failed_user_inactive": "帳號停用登入失敗",
|
||||
"txt_log_action_auth_login_success": "登入成功",
|
||||
"txt_log_action_auth_refresh_failed": "刷新登入失敗:{reason}",
|
||||
"txt_log_action_cipher_delete_permanent": "永久刪除密碼項",
|
||||
"txt_log_action_cipher_delete_permanent_bulk": "批次永久刪除密碼項",
|
||||
"txt_log_action_cipher_delete_soft": "刪除到回收桶",
|
||||
"txt_log_action_cipher_delete_soft_bulk": "批次刪除到回收桶",
|
||||
"txt_log_action_device_deactivate": "停用裝置",
|
||||
"txt_log_action_device_delete": "刪除裝置",
|
||||
"txt_log_action_device_delete_all": "刪除全部裝置",
|
||||
"txt_log_action_device_name_update": "修改裝置名稱",
|
||||
"txt_log_action_device_trust_permanent": "永久信任裝置",
|
||||
"txt_log_action_device_trust_revoke": "撤銷裝置信任",
|
||||
"txt_log_action_device_trust_revoke_batch": "批次撤銷裝置信任",
|
||||
"txt_log_action_folder_delete": "刪除資料夾",
|
||||
"txt_log_action_folder_delete_bulk": "批次刪除資料夾",
|
||||
"txt_log_action_send_auth_remove": "移除 Send 驗證",
|
||||
"txt_log_action_send_delete": "刪除 Send",
|
||||
"txt_log_action_send_delete_bulk": "批次刪除 Send",
|
||||
"txt_log_action_send_password_remove": "移除 Send 密碼",
|
||||
"txt_log_action_user_password_change": "修改主密碼",
|
||||
"txt_log_action_user_register_first_admin": "註冊首個管理員",
|
||||
"txt_log_action_user_register_invite": "透過邀請註冊",
|
||||
"txt_log_meta_attachments": "附件數",
|
||||
"txt_log_meta_bytes": "位元組數",
|
||||
"txt_log_meta_changed": "變更項",
|
||||
"txt_log_meta_checksum_mismatch_accepted": "已接受校驗不一致",
|
||||
"txt_log_meta_cipher_id": "密碼項 ID",
|
||||
"txt_log_meta_ciphers": "密碼項數量",
|
||||
"txt_log_meta_compat": "相容資訊",
|
||||
"txt_log_meta_compressed_bytes": "壓縮後位元組數",
|
||||
"txt_log_meta_count": "數量",
|
||||
"txt_log_meta_deleted": "已刪除數量",
|
||||
"txt_log_meta_destination_count": "備份目標數量",
|
||||
"txt_log_meta_destination_id": "備份目標 ID",
|
||||
"txt_log_meta_destination_name": "備份目標名稱",
|
||||
"txt_log_meta_destination_type": "備份目標類型",
|
||||
"txt_log_meta_device_identifier": "裝置 ID",
|
||||
"txt_log_meta_device_type": "裝置類型",
|
||||
"txt_log_meta_email": "信箱",
|
||||
"txt_log_meta_error": "錯誤",
|
||||
"txt_log_meta_expires_in_hours": "過期小時數",
|
||||
"txt_log_meta_file_bytes": "檔案位元組數",
|
||||
"txt_log_meta_file_name": "檔案名稱",
|
||||
"txt_log_meta_folder_id": "資料夾 ID",
|
||||
"txt_log_meta_grant_type": "登入方式",
|
||||
"txt_log_meta_includes_attachments": "包含附件",
|
||||
"txt_log_meta_ip": "IP 位址",
|
||||
"txt_log_meta_max_entries": "筆數上限",
|
||||
"txt_log_meta_method": "請求方法",
|
||||
"txt_log_meta_path": "請求路徑",
|
||||
"txt_log_meta_provider": "服務提供方",
|
||||
"txt_log_meta_prune_error": "清理錯誤",
|
||||
"txt_log_meta_pruned_file_count": "已清理檔案數",
|
||||
"txt_log_meta_raw": "原始資料",
|
||||
"txt_log_meta_reason": "原因",
|
||||
"txt_log_meta_remote_path": "遠端路徑",
|
||||
"txt_log_meta_removed": "已移除數量",
|
||||
"txt_log_meta_removed_devices": "已移除裝置數",
|
||||
"txt_log_meta_removed_sessions": "已移除工作階段數",
|
||||
"txt_log_meta_removed_trusted": "已撤銷信任數",
|
||||
"txt_log_meta_replace_existing": "覆蓋現有資料",
|
||||
"txt_log_meta_requested": "請求數量",
|
||||
"txt_log_meta_requested_count": "請求數量",
|
||||
"txt_log_meta_retention_days": "保留天數",
|
||||
"txt_log_meta_scheduled_destination_count": "已排程備份目標數",
|
||||
"txt_log_meta_size": "大小",
|
||||
"txt_log_meta_skipped_attachments": "略過附件數",
|
||||
"txt_log_meta_skipped_reason": "略過原因",
|
||||
"txt_log_meta_status": "狀態",
|
||||
"txt_log_meta_target_email": "目標信箱",
|
||||
"txt_log_meta_trigger": "觸發方式",
|
||||
"txt_log_meta_type": "類型",
|
||||
"txt_log_meta_updated": "已更新數量",
|
||||
"txt_log_meta_upload_verification_attempts": "上傳校驗次數",
|
||||
"txt_log_meta_user_agent": "瀏覽器/用戶端",
|
||||
"txt_log_meta_users": "使用者數量",
|
||||
"txt_log_meta_verify_devices": "驗證裝置",
|
||||
"txt_log_meta_web_session": "網頁工作階段",
|
||||
"txt_log_reason_bad_api_key": "API 金鑰錯誤",
|
||||
"txt_log_reason_bad_password": "密碼錯誤",
|
||||
"txt_log_reason_device_missing": "裝置不存在",
|
||||
"txt_log_reason_device_session_mismatch": "裝置工作階段不相符",
|
||||
"txt_log_reason_token_not_found_or_expired": "權杖不存在或已過期",
|
||||
"txt_log_reason_user_inactive": "使用者未啟用",
|
||||
"txt_log_reason_user_missing": "使用者不存在",
|
||||
"txt_log_target_type_attachment": "附件",
|
||||
"txt_log_target_type_audit_log": "日誌",
|
||||
"txt_log_target_type_backup": "備份",
|
||||
"txt_log_target_type_cipher": "密碼項",
|
||||
"txt_log_target_type_device": "裝置",
|
||||
"txt_log_target_type_folder": "資料夾",
|
||||
"txt_log_target_type_invite": "邀請",
|
||||
"txt_log_target_type_refresh_token": "刷新權杖",
|
||||
"txt_log_target_type_send": "Send",
|
||||
"txt_log_target_type_user": "使用者",
|
||||
"txt_log_trigger_manual": "手動",
|
||||
"txt_log_trigger_remote": "遠端",
|
||||
"txt_log_trigger_scheduled": "排程工作",
|
||||
"txt_log_max_1000": "最多 1,000 筆",
|
||||
"txt_log_max_5000": "最多 5,000 筆",
|
||||
"txt_log_max_10000": "最多 10,000 筆",
|
||||
"txt_log_max_50000": "最多 50,000 筆",
|
||||
"txt_log_max_entries": "容量上限",
|
||||
"txt_log_max_unlimited": "不限制筆數",
|
||||
"txt_log_retention_7d": "保留 7 天",
|
||||
"txt_log_retention_30d": "保留 30 天",
|
||||
"txt_log_retention_90d": "保留 90 天",
|
||||
"txt_log_retention_180d": "保留 180 天",
|
||||
"txt_log_retention_365d": "保留 365 天",
|
||||
"txt_log_retention_days": "保留時間",
|
||||
"txt_log_retention_forever": "永久保留",
|
||||
"txt_log_retention_hint": "按時間和最大筆數自動收縮,減少 D1 儲存占用。",
|
||||
"txt_log_retention_mode": "保留方式",
|
||||
"txt_log_retention_mode_days": "按時間",
|
||||
"txt_log_retention_mode_entries": "按筆數",
|
||||
"txt_log_retention_settings": "日誌保留",
|
||||
"txt_log_settings": "設定",
|
||||
"txt_log_settings_save_failed": "儲存日誌設定失敗",
|
||||
"txt_log_settings_saved": "日誌設定已儲存",
|
||||
"txt_log_search_placeholder": "搜尋動作、操作者、目標、請求路徑或元資料",
|
||||
"txt_log_total": " 條總數",
|
||||
"txt_log_visible": " 條顯示",
|
||||
"txt_metadata": "元資料",
|
||||
"txt_no_logs_found": "沒有找到日誌",
|
||||
"txt_no_metadata": "沒有元資料",
|
||||
"txt_clear_all_logs": "清空日誌",
|
||||
"txt_clear_logs_confirm": "確定清空全部日誌嗎?此操作無法復原。",
|
||||
"txt_clear_logs_failed": "清空日誌失敗",
|
||||
"txt_logs_cleared": "日誌已清空",
|
||||
"txt_search": "搜尋",
|
||||
"txt_target": "目標",
|
||||
"txt_time": "時間",
|
||||
"txt_time_range": "時間範圍",
|
||||
"txt_remove_domain": "移除域名"
|
||||
};
|
||||
|
||||
|
||||
@@ -281,6 +281,11 @@ export interface VaultDraft {
|
||||
export interface ListResponse<T> {
|
||||
object: 'list';
|
||||
data: T[];
|
||||
total?: number;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
hasMore?: boolean;
|
||||
continuationToken?: string | null;
|
||||
}
|
||||
|
||||
export interface WebBootstrapResponse {
|
||||
@@ -344,6 +349,37 @@ export interface AdminInvite {
|
||||
expiresAt?: string;
|
||||
}
|
||||
|
||||
export type AuditLogCategory = 'auth' | 'security' | 'device' | 'data' | 'system';
|
||||
export type AuditLogLevel = 'info' | 'warn' | 'error' | 'security';
|
||||
|
||||
export interface AuditLogEntry {
|
||||
id: string;
|
||||
actorUserId: string | null;
|
||||
actorEmail?: string | null;
|
||||
action: string;
|
||||
category: AuditLogCategory;
|
||||
level: AuditLogLevel;
|
||||
targetType: string | null;
|
||||
targetId: string | null;
|
||||
targetUserEmail?: string | null;
|
||||
metadata: string | null;
|
||||
createdAt: string;
|
||||
object?: 'auditLog';
|
||||
}
|
||||
|
||||
export interface AuditLogSettings {
|
||||
retentionDays: number | null;
|
||||
maxEntries: number | null;
|
||||
}
|
||||
|
||||
export interface AuditLogListResult {
|
||||
logs: AuditLogEntry[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
export interface AuthorizedDevice {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -336,3 +336,33 @@
|
||||
background: color-mix(in srgb, var(--primary) 18%, var(--panel));
|
||||
color: var(--primary-strong);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .log-detail-head h3,
|
||||
:root[data-theme='dark'] .log-row-main strong,
|
||||
:root[data-theme='dark'] .log-detail-meta strong,
|
||||
:root[data-theme='dark'] .log-detail-json dd,
|
||||
:root[data-theme='dark'] .log-detail-json h4,
|
||||
:root[data-theme='dark'] .log-pagination-count {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .log-row-main small,
|
||||
:root[data-theme='dark'] .log-detail-meta span,
|
||||
:root[data-theme='dark'] .log-detail-json dt {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .log-row,
|
||||
:root[data-theme='dark'] .log-detail-meta > div,
|
||||
:root[data-theme='dark'] .log-detail-json dl > div,
|
||||
:root[data-theme='dark'] .log-pagination-count {
|
||||
background: var(--panel-muted);
|
||||
border-color: var(--line);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .log-row:hover,
|
||||
:root[data-theme='dark'] .log-row.active {
|
||||
background: color-mix(in srgb, var(--primary) 12%, var(--panel));
|
||||
border-color: color-mix(in srgb, var(--primary) 34%, var(--line));
|
||||
}
|
||||
|
||||
@@ -503,6 +503,533 @@
|
||||
grid-template-columns: repeat(2, minmax(0, calc((100% - var(--settings-grid-gap)) / 2)));
|
||||
}
|
||||
|
||||
.log-center-page {
|
||||
@apply grid h-full min-h-0 gap-3;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card.log-center-toolbar {
|
||||
@apply relative;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.log-mobile-subhead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.log-detail-head h3 {
|
||||
@apply m-0 text-base font-extrabold;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.log-filter-form {
|
||||
@apply grid items-end gap-3;
|
||||
grid-template-columns: minmax(260px, 1.5fr) repeat(3, minmax(150px, 0.66fr)) auto;
|
||||
}
|
||||
|
||||
.log-filter-form .field {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
.log-filter-form .input,
|
||||
.log-filter-form .btn {
|
||||
min-height: 42px;
|
||||
}
|
||||
|
||||
.log-search-field {
|
||||
@apply min-w-0;
|
||||
}
|
||||
|
||||
.input-leading-icon {
|
||||
@apply pointer-events-none absolute left-3 top-1/2 -translate-y-1/2;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.log-search-input {
|
||||
padding-left: 2.25rem;
|
||||
}
|
||||
|
||||
.log-filter-actions {
|
||||
@apply flex-nowrap items-end;
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
.log-filter-actions .btn {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.log-settings-popover {
|
||||
@apply absolute right-3 z-30 grid gap-3 rounded-xl border p-3;
|
||||
top: calc(100% + 8px);
|
||||
width: min(390px, calc(100vw - 32px));
|
||||
border-color: var(--line);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 18px 44px rgba(15, 23, 42, 0.16);
|
||||
}
|
||||
|
||||
.log-settings-popover-head {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
.log-settings-popover-head h3 {
|
||||
@apply m-0 text-base font-extrabold;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.log-settings-mode {
|
||||
@apply grid rounded-lg p-1;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.log-mode-option {
|
||||
@apply h-9 cursor-pointer rounded-md border-0 px-2 text-sm font-extrabold;
|
||||
background: transparent;
|
||||
color: #475569;
|
||||
transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.log-mode-option.active {
|
||||
background: #ffffff;
|
||||
color: #1d4ed8;
|
||||
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
.log-mode-option:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.log-settings-retention-block {
|
||||
@apply grid gap-1.5;
|
||||
}
|
||||
|
||||
.log-settings-label {
|
||||
@apply block text-[13px] font-bold;
|
||||
color: var(--muted-strong);
|
||||
}
|
||||
|
||||
.log-settings-retention-row {
|
||||
@apply grid items-center gap-2.5;
|
||||
grid-template-columns: minmax(0, 1fr) 82px;
|
||||
}
|
||||
|
||||
.log-settings-retention-row .input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 42px;
|
||||
min-height: 42px;
|
||||
}
|
||||
|
||||
.log-settings-save-btn.btn {
|
||||
width: 82px;
|
||||
height: 42px;
|
||||
min-height: 42px;
|
||||
align-self: center;
|
||||
justify-content: center;
|
||||
padding-inline: 10px;
|
||||
white-space: nowrap;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.log-settings-save-btn.btn:hover:not(:disabled),
|
||||
.log-settings-save-btn.btn:active:not(:disabled) {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.log-settings-danger {
|
||||
@apply grid gap-2 border-t pt-3;
|
||||
border-color: var(--line);
|
||||
}
|
||||
|
||||
.log-settings-danger p {
|
||||
@apply m-0 text-sm font-semibold leading-5;
|
||||
color: #7f1d1d;
|
||||
}
|
||||
|
||||
.ghost-danger {
|
||||
@apply w-full justify-center;
|
||||
}
|
||||
|
||||
.log-clear-confirm-actions {
|
||||
@apply grid grid-cols-2;
|
||||
}
|
||||
|
||||
.log-center-grid {
|
||||
@apply grid min-h-0 items-stretch gap-3;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card.log-list-panel,
|
||||
.card.log-detail-panel {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
margin-bottom: 0;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card.log-list-panel {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card.log-detail-panel {
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
.log-list {
|
||||
@apply grid content-start gap-2 overflow-auto pr-0.5;
|
||||
min-height: 0;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
.log-list-panel > .section-head,
|
||||
.log-pagination {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log-row {
|
||||
@apply grid w-full cursor-pointer items-center gap-3 rounded-xl p-3 text-left;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
border: 1px solid var(--line);
|
||||
background: #ffffff;
|
||||
transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.log-row:hover,
|
||||
.log-row.active {
|
||||
border-color: #93c5fd;
|
||||
background: #f8fbff;
|
||||
box-shadow: 0 10px 22px rgba(37, 99, 235, 0.08);
|
||||
}
|
||||
|
||||
.log-row-icon {
|
||||
@apply flex h-9 w-9 items-center justify-center rounded-xl;
|
||||
}
|
||||
|
||||
.log-category-auth {
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.log-category-security {
|
||||
background: #fff1f2;
|
||||
color: #be123c;
|
||||
}
|
||||
|
||||
.log-category-device {
|
||||
background: #ecfdf5;
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.log-category-data {
|
||||
background: #f5f3ff;
|
||||
color: #6d28d9;
|
||||
}
|
||||
|
||||
.log-category-system {
|
||||
background: #f8fafc;
|
||||
color: #475467;
|
||||
}
|
||||
|
||||
.log-row-main {
|
||||
@apply grid min-w-0 gap-1;
|
||||
}
|
||||
|
||||
.log-row-main strong {
|
||||
@apply truncate text-sm;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.log-row-main small {
|
||||
@apply text-xs;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.log-level-pill {
|
||||
@apply inline-flex whitespace-nowrap rounded-full px-2.5 py-1 text-xs font-extrabold;
|
||||
}
|
||||
|
||||
.log-level-info {
|
||||
background: #eef4ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.log-level-warn {
|
||||
background: #fff7ed;
|
||||
color: #c2410c;
|
||||
}
|
||||
|
||||
.log-level-error {
|
||||
background: #fef2f2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.log-level-security {
|
||||
background: #fff1f2;
|
||||
color: #be123c;
|
||||
}
|
||||
|
||||
.log-pagination {
|
||||
@apply mt-3 items-center justify-between;
|
||||
}
|
||||
|
||||
.log-pagination-count {
|
||||
@apply inline-flex min-w-24 items-center justify-center rounded-full px-3 py-1.5 text-sm font-extrabold;
|
||||
border: 1px solid var(--line);
|
||||
background: #f8fafc;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.log-detail-meta {
|
||||
@apply grid gap-2;
|
||||
}
|
||||
|
||||
.log-detail-meta > div,
|
||||
.log-detail-json dl > div {
|
||||
@apply grid gap-1 rounded-xl px-3 py-2.5;
|
||||
border: 1px solid var(--line);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.log-detail-meta span,
|
||||
.log-detail-json dt {
|
||||
@apply text-xs font-bold uppercase;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.log-detail-meta strong,
|
||||
.log-detail-json dd {
|
||||
@apply m-0 min-w-0 text-sm font-semibold;
|
||||
color: #0f172a;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.log-detail-json {
|
||||
@apply mt-3 grid gap-2;
|
||||
}
|
||||
|
||||
.log-detail-json h4 {
|
||||
@apply m-0 text-sm font-extrabold;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.log-detail-json dl {
|
||||
@apply m-0 grid gap-2;
|
||||
}
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
.log-filter-form {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.log-filter-actions {
|
||||
@apply col-span-2;
|
||||
}
|
||||
|
||||
.log-center-grid {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: repeat(2, minmax(220px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.route-stage-log-fixed {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.log-center-page {
|
||||
gap: 8px;
|
||||
grid-template-rows: auto auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.log-center-page.log-mobile-detail-open {
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.log-mobile-subhead {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
min-height: 38px;
|
||||
flex-shrink: 0;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.log-mobile-subhead .mobile-settings-back {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.log-mobile-settings-trigger {
|
||||
width: 42px;
|
||||
height: 38px;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.log-mobile-settings-trigger .btn-icon {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.log-mobile-detail-open .log-mobile-settings-trigger {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.card.log-center-toolbar {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.log-mobile-detail-open .card.log-center-toolbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.log-filter-form {
|
||||
gap: 6px;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.log-filter-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.log-search-field > span {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.log-search-field {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.log-filter-form .field {
|
||||
margin-bottom: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.log-filter-form > .field:not(.log-search-field) > span {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.log-filter-form .input {
|
||||
min-height: 40px;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
padding-inline: 9px 26px;
|
||||
}
|
||||
|
||||
.log-search-input {
|
||||
font-size: 14px;
|
||||
padding-left: 2.15rem;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.log-filter-form select.input {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.log-center-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.card.log-list-panel {
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.log-list-panel > .section-head,
|
||||
.log-pagination {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.card.log-detail-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.log-mobile-detail-open .card.log-list-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.log-mobile-detail-open .card.log-detail-panel {
|
||||
display: block;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
padding: 10px 12px 14px;
|
||||
}
|
||||
|
||||
.log-settings-popover {
|
||||
@apply static mt-3 w-full;
|
||||
}
|
||||
|
||||
.log-settings-retention-row {
|
||||
grid-template-columns: minmax(0, 1fr) 82px;
|
||||
}
|
||||
|
||||
.log-row {
|
||||
min-height: 66px;
|
||||
gap: 12px;
|
||||
grid-template-columns: 38px minmax(0, 1fr) auto;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.log-row .log-level-pill {
|
||||
grid-column: auto;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.log-row-icon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.log-row-main {
|
||||
justify-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.log-row-main strong {
|
||||
max-width: 100%;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.log-row-main small {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-module {
|
||||
@apply min-w-0;
|
||||
width: 100%;
|
||||
|
||||
@@ -316,6 +316,12 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.route-stage-log-fixed {
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mobile-sidebar-mask {
|
||||
@apply pointer-events-none invisible fixed inset-0 opacity-0;
|
||||
background: rgba(15, 23, 42, 0.36);
|
||||
|
||||
Reference in New Issue
Block a user