diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index 4627e9b..ba2576a 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -53,6 +53,17 @@ import { APP_NOTIFY_EVENT, type AppNotifyDetail } from '@/lib/app-notify'; import { dispatchBackupProgress, type BackupProgressDetail } from '@/lib/backup-restore-progress'; import type { AppPhase, Cipher, Folder as VaultFolder, Profile, Send, SessionState } from '@/lib/types'; +function isBackupProgressDetail(value: unknown): value is BackupProgressDetail { + if (!value || typeof value !== 'object') return false; + const detail = value as Record; + const operation = detail.operation; + return ( + (operation === 'backup-restore' || operation === 'backup-export' || operation === 'backup-remote-run') + && typeof detail.step === 'string' + && typeof detail.fileName === 'string' + ); +} + const IMPORT_ROUTE = '/backup/import-export'; const IMPORT_ROUTE_PATHS = [IMPORT_ROUTE, '/tools/import', '/tools/import-export', '/tools/import-data', '/import', '/import-export'] as const; const IMPORT_ROUTE_ALIASES: ReadonlySet = new Set(IMPORT_ROUTE_PATHS.filter((path) => path !== IMPORT_ROUTE)); @@ -927,17 +938,7 @@ export default function App() { } if (updateType === SIGNALR_UPDATE_TYPE_BACKUP_RESTORE_PROGRESS) { const payload = frame.arguments?.[0]?.Payload; - if ( - payload - && typeof payload === 'object' - && ( - payload.operation === 'backup-restore' - || payload.operation === 'backup-export' - || payload.operation === 'backup-remote-run' - ) - ) { - dispatchBackupProgress(payload as BackupProgressDetail); - } + if (isBackupProgressDetail(payload)) dispatchBackupProgress(payload); continue; } if (updateType !== SIGNALR_UPDATE_TYPE_SYNC_VAULT) continue; diff --git a/webapp/src/components/AdminPage.tsx b/webapp/src/components/AdminPage.tsx index d9621ce..6307fce 100644 --- a/webapp/src/components/AdminPage.tsx +++ b/webapp/src/components/AdminPage.tsx @@ -40,6 +40,12 @@ export default function AdminPage(props: AdminPageProps) { return status || '-'; }; + const normalizeToggleableStatus = (status: string): 'active' | 'banned' | null => { + const normalized = String(status || '').toLowerCase(); + if (normalized === 'active' || normalized === 'banned') return normalized; + return null; + }; + return (
@@ -55,8 +61,10 @@ export default function AdminPage(props: AdminPageProps) { - {props.users.map((user) => ( - + {props.users.map((user) => { + const toggleableStatus = normalizeToggleableStatus(user.status); + return ( + {user.email} {user.name || t('txt_dash')} {roleText(user.role)} @@ -66,8 +74,11 @@ export default function AdminPage(props: AdminPageProps) {
- - ))} + + ); + })} diff --git a/webapp/src/components/BackupCenterPage.tsx b/webapp/src/components/BackupCenterPage.tsx index 9c46df7..e57e17b 100644 --- a/webapp/src/components/BackupCenterPage.tsx +++ b/webapp/src/components/BackupCenterPage.tsx @@ -625,7 +625,7 @@ export default function BackupCenterPage(props: BackupCenterPageProps) { setSettings(result.settings); setSelectedDestinationId(selectedDestination.id); await loadRemoteBrowser(selectedDestination.id, currentRemoteBrowserPath, { force: true }); - props.onNotify('success', t('txt_backup_remote_run_success_verified', { name: result.fileName })); + props.onNotify('success', t('txt_backup_remote_run_success_verified', { name: result.result.fileName })); } catch (error) { const message = error instanceof Error ? error.message : t('txt_backup_remote_run_failed'); setLocalError(message); diff --git a/webapp/src/components/PublicSendPage.tsx b/webapp/src/components/PublicSendPage.tsx index 81d319a..a1b15fa 100644 --- a/webapp/src/components/PublicSendPage.tsx +++ b/webapp/src/components/PublicSendPage.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'preact/hooks'; import { Download, Eye, Lock } from 'lucide-preact'; import { accessPublicSend, accessPublicSendFile, decryptPublicSend, decryptPublicSendFileBytes } from '@/lib/api/send'; +import { toBufferSource } from '@/lib/crypto'; import { downloadBytesAsFile, readResponseBytesWithProgress } from '@/lib/download'; import StandalonePageFrame from '@/components/StandalonePageFrame'; import { t } from '@/lib/i18n'; @@ -61,13 +62,13 @@ export default function PublicSendPage(props: PublicSendPageProps) { if (props.keyPart) { try { const decryptedBytes = await decryptPublicSendFileBytes(encryptedBytes, props.keyPart); - blob = new Blob([decryptedBytes as unknown as BlobPart], { type: 'application/octet-stream' }); + blob = new Blob([toBufferSource(decryptedBytes)], { type: 'application/octet-stream' }); } catch { // Legacy compatibility: early web-created file sends uploaded plaintext bytes. - blob = new Blob([encryptedBytes], { type: 'application/octet-stream' }); + blob = new Blob([toBufferSource(encryptedBytes)], { type: 'application/octet-stream' }); } } else { - blob = new Blob([encryptedBytes], { type: 'application/octet-stream' }); + blob = new Blob([toBufferSource(encryptedBytes)], { type: 'application/octet-stream' }); } downloadBytesAsFile( new Uint8Array(await blob.arrayBuffer()), diff --git a/webapp/src/components/TotpCodesPage.tsx b/webapp/src/components/TotpCodesPage.tsx index b18a212..a3b5a85 100644 --- a/webapp/src/components/TotpCodesPage.tsx +++ b/webapp/src/components/TotpCodesPage.tsx @@ -1,3 +1,4 @@ +import type { JSX } from 'preact'; import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; import { Clipboard, Globe, GripVertical } from 'lucide-preact'; import { @@ -96,6 +97,7 @@ function SortableTotpRow(props: SortableTotpRowProps) { const { attributes, listeners, setActivatorNodeRef, setNodeRef, transform, transition, isDragging } = useSortable({ id: props.cipher.id, }); + const dragButtonAttributes = attributes as JSX.HTMLAttributes; const style = { transform: CSS.Transform.toString(transform), @@ -113,7 +115,7 @@ function SortableTotpRow(props: SortableTotpRowProps) { className="btn btn-secondary small totp-drag-btn" title={t('txt_drag_to_reorder')} aria-label={t('txt_drag_to_reorder')} - {...attributes} + {...dragButtonAttributes} {...listeners} > diff --git a/webapp/src/components/backup-center/BackupDestinationDetail.tsx b/webapp/src/components/backup-center/BackupDestinationDetail.tsx index 7a27293..25f2515 100644 --- a/webapp/src/components/backup-center/BackupDestinationDetail.tsx +++ b/webapp/src/components/backup-center/BackupDestinationDetail.tsx @@ -134,6 +134,7 @@ export function BackupDestinationDetail(props: BackupDestinationDetailProps) { ...COMMON_TIME_ZONES, ...props.availableTimeZones, ])); + const selectedIntervalHours = props.selectedDestination?.schedule.intervalHours ?? 24; if (props.selectedRecommendedProvider) { return ( @@ -216,7 +217,7 @@ export function BackupDestinationDetail(props: BackupDestinationDetailProps) { type="text" inputMode="numeric" pattern="[0-9]*" - value={String(props.selectedDestination.schedule.intervalHours || 24)} + value={String(selectedIntervalHours)} disabled={props.loadingSettings || props.disableWhileBusy} onInput={(event) => { const raw = (event.currentTarget as HTMLInputElement).value.replace(/[^\d]/g, ''); @@ -234,7 +235,7 @@ export function BackupDestinationDetail(props: BackupDestinationDetailProps) {
{INTERVAL_HOUR_PRESETS.map((preset) => { - const active = preset === props.selectedDestination.schedule.intervalHours; + const active = preset === selectedIntervalHours; return (