refactor(ts): convert admin/ cards to TypeScript (AuthMethods, BankSync, Backup, ...)
Completes the entire client/components/admin/ directory conversion to .tsx: OidcForm/AuthConfigData/OidcTest types for the OIDC config form; Backup/ BackupSettings types for the backup manager. errMessage() on all strict catch(unknown) sites. typecheck 0, lint 0 errors, build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c9b5f93569
commit
a8496a9c64
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/api';
|
||||
import { errMessage } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||
|
|
@ -8,29 +9,58 @@ import { FieldRow, Toggle } from './adminShared';
|
|||
|
||||
const AUTHENTIK_ICON_URL = '/img/auth.png';
|
||||
|
||||
function defaultOidcRedirectUri() {
|
||||
interface OidcForm {
|
||||
local_login_enabled: boolean;
|
||||
oidc_login_enabled: boolean;
|
||||
oidc_provider_name: string;
|
||||
oidc_issuer_url: string;
|
||||
oidc_client_id: string;
|
||||
oidc_client_secret: string;
|
||||
oidc_client_secret_clear: boolean;
|
||||
oidc_token_auth_method: string;
|
||||
oidc_redirect_uri: string;
|
||||
oidc_scopes: string;
|
||||
oidc_auto_provision: boolean;
|
||||
oidc_admin_group: string;
|
||||
oidc_default_role: string;
|
||||
}
|
||||
|
||||
interface AuthConfigData {
|
||||
local_login_enabled?: boolean;
|
||||
oidc_login_enabled?: boolean;
|
||||
oidc_provider_name?: string;
|
||||
oidc_issuer_url?: string;
|
||||
oidc_client_id?: string;
|
||||
oidc_token_auth_method?: string;
|
||||
oidc_redirect_uri?: string;
|
||||
oidc_scopes?: string;
|
||||
oidc_auto_provision?: boolean;
|
||||
oidc_admin_group?: string;
|
||||
oidc_default_role?: string;
|
||||
oidc_client_secret_set?: boolean;
|
||||
oidc_configured?: boolean;
|
||||
oidc_env_fallback_used?: boolean;
|
||||
warnings?: string[];
|
||||
}
|
||||
|
||||
interface OidcTestResult {
|
||||
ok?: boolean;
|
||||
error?: string;
|
||||
issuer?: string;
|
||||
}
|
||||
|
||||
function defaultOidcRedirectUri(): string {
|
||||
if (typeof window === 'undefined') return '';
|
||||
return `${window.location.origin}/api/auth/oidc/callback`;
|
||||
}
|
||||
|
||||
function looksLikeOidcEndpoint(url) {
|
||||
function looksLikeOidcEndpoint(url: string | null | undefined): boolean {
|
||||
const value = String(url || '').toLowerCase();
|
||||
return /\/(?:authorize|token|userinfo|jwks|certs)\/?$/.test(value);
|
||||
}
|
||||
|
||||
export default function AuthMethodsCard() {
|
||||
const [data, setData] = useState(null);
|
||||
const [form, setForm] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [testingOidc, setTestingOidc] = useState(false);
|
||||
const [oidcTest, setOidcTest] = useState(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const d = await api.authModeConfig();
|
||||
setData(d);
|
||||
setForm({
|
||||
function formFromData(d: AuthConfigData): OidcForm {
|
||||
return {
|
||||
local_login_enabled: d.local_login_enabled !== false,
|
||||
oidc_login_enabled: !!d.oidc_login_enabled,
|
||||
oidc_provider_name: d.oidc_provider_name || 'authentik',
|
||||
|
|
@ -44,9 +74,24 @@ export default function AuthMethodsCard() {
|
|||
oidc_auto_provision: d.oidc_auto_provision !== false,
|
||||
oidc_admin_group: d.oidc_admin_group || '',
|
||||
oidc_default_role: d.oidc_default_role || 'user',
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default function AuthMethodsCard() {
|
||||
const [data, setData] = useState<AuthConfigData | null>(null);
|
||||
const [form, setForm] = useState<OidcForm | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [testingOidc, setTestingOidc] = useState(false);
|
||||
const [oidcTest, setOidcTest] = useState<OidcTestResult | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const d = await api.authModeConfig() as AuthConfigData;
|
||||
setData(d);
|
||||
setForm(formFromData(d));
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to load auth settings.');
|
||||
toast.error(errMessage(err, 'Failed to load auth settings.'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -54,31 +99,17 @@ export default function AuthMethodsCard() {
|
|||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const set = (k, v) => setForm(prev => ({ ...prev, [k]: v }));
|
||||
const set = (k: keyof OidcForm, v: string | boolean) => setForm(prev => prev ? ({ ...prev, [k]: v }) as OidcForm : prev);
|
||||
|
||||
async function handleSave() {
|
||||
setSaving(true);
|
||||
try {
|
||||
const d = await api.setAuthMode(form);
|
||||
const d = await api.setAuthMode(form) as AuthConfigData;
|
||||
setData(d);
|
||||
setForm({
|
||||
local_login_enabled: d.local_login_enabled !== false,
|
||||
oidc_login_enabled: !!d.oidc_login_enabled,
|
||||
oidc_provider_name: d.oidc_provider_name || 'authentik',
|
||||
oidc_issuer_url: d.oidc_issuer_url || '',
|
||||
oidc_client_id: d.oidc_client_id || '',
|
||||
oidc_client_secret: '',
|
||||
oidc_client_secret_clear: false,
|
||||
oidc_token_auth_method: d.oidc_token_auth_method || 'client_secret_basic',
|
||||
oidc_redirect_uri: d.oidc_redirect_uri || defaultOidcRedirectUri(),
|
||||
oidc_scopes: d.oidc_scopes || 'openid email profile groups',
|
||||
oidc_auto_provision: d.oidc_auto_provision !== false,
|
||||
oidc_admin_group: d.oidc_admin_group || '',
|
||||
oidc_default_role: d.oidc_default_role || 'user',
|
||||
});
|
||||
setForm(formFromData(d));
|
||||
toast.success('Auth method settings saved.');
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to save auth method settings.');
|
||||
toast.error(errMessage(err, 'Failed to save auth method settings.'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
|
|
@ -88,11 +119,12 @@ export default function AuthMethodsCard() {
|
|||
setTestingOidc(true);
|
||||
setOidcTest(null);
|
||||
try {
|
||||
const result = await api.testOidcConfig(form);
|
||||
const result = await api.testOidcConfig(form) as OidcTestResult;
|
||||
setOidcTest(result);
|
||||
toast.success('authentik configuration test passed.');
|
||||
} catch (err) {
|
||||
const result = err.data || { ok: false, error: err.message || 'OIDC configuration test failed.' };
|
||||
const e = err as { data?: OidcTestResult; message?: string };
|
||||
const result = e.data || { ok: false, error: e.message || 'OIDC configuration test failed.' };
|
||||
setOidcTest(result);
|
||||
toast.error(result.error || 'OIDC configuration test failed.');
|
||||
} finally {
|
||||
|
|
@ -151,7 +183,7 @@ export default function AuthMethodsCard() {
|
|||
|
||||
<CardContent className="space-y-5">
|
||||
|
||||
{(data?.warnings?.length > 0 || wouldLockOut || cantDisableLocal) && (
|
||||
{((data?.warnings?.length ?? 0) > 0 || wouldLockOut || cantDisableLocal) && (
|
||||
<div className="rounded-lg border border-amber-500/25 bg-amber-500/10 px-4 py-3 space-y-1">
|
||||
{wouldLockOut && (
|
||||
<p className="text-sm text-amber-600 dark:text-amber-400">
|
||||
|
|
@ -229,11 +261,11 @@ export default function AuthMethodsCard() {
|
|||
<Input
|
||||
type="password"
|
||||
value={form.oidc_client_secret}
|
||||
onChange={e => setForm(prev => ({
|
||||
onChange={e => setForm(prev => prev ? ({
|
||||
...prev,
|
||||
oidc_client_secret: e.target.value,
|
||||
oidc_client_secret_clear: e.target.value ? false : prev.oidc_client_secret_clear,
|
||||
}))}
|
||||
}) : prev)}
|
||||
placeholder="Leave blank to keep existing secret"
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Database, Download, Play, RefreshCw, RotateCcw, Trash2, Upload } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/api';
|
||||
import { cn, fmtBytes } from '@/lib/utils';
|
||||
import { cn, fmtBytes, errMessage } from '@/lib/utils';
|
||||
import { Button, buttonVariants } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Input } from '@/components/ui/input';
|
||||
|
|
@ -18,7 +18,25 @@ import {
|
|||
import { SectionHeading, Toggle, formatDateTime, BackupTypeBadge } from './adminShared';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
interface Backup {
|
||||
id: string;
|
||||
type?: string;
|
||||
modified_at?: string | null;
|
||||
size_bytes?: number;
|
||||
checksum?: string | null;
|
||||
}
|
||||
|
||||
interface BackupSettings {
|
||||
enabled: boolean;
|
||||
frequency: string;
|
||||
time: string;
|
||||
retention_count: number | string;
|
||||
last_run_at: string | null;
|
||||
next_run_at: string | null;
|
||||
last_error: string | null;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: BackupSettings = {
|
||||
enabled: false,
|
||||
frequency: 'daily',
|
||||
time: '02:00',
|
||||
|
|
@ -29,24 +47,24 @@ const DEFAULT_SETTINGS = {
|
|||
};
|
||||
|
||||
export default function BackupManagementCard() {
|
||||
const [backups, setBackups] = useState([]);
|
||||
const [settings, setSettings] = useState(DEFAULT_SETTINGS);
|
||||
const [backups, setBackups] = useState<Backup[]>([]);
|
||||
const [settings, setSettings] = useState<BackupSettings>(DEFAULT_SETTINGS);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState('');
|
||||
const [restoreTarget, setRestoreTarget] = useState(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState(null);
|
||||
const [restoreTarget, setRestoreTarget] = useState<Backup | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<Backup | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [backupData, settingsData] = await Promise.all([
|
||||
api.adminBackups(),
|
||||
api.adminBackupSettings(),
|
||||
api.adminBackups() as Promise<{ backups?: Backup[] }>,
|
||||
api.adminBackupSettings() as Promise<Partial<BackupSettings>>,
|
||||
]);
|
||||
setBackups(backupData.backups || []);
|
||||
setSettings({ ...DEFAULT_SETTINGS, ...settingsData });
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to load backups.');
|
||||
toast.error(errMessage(err, 'Failed to load backups.'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -55,7 +73,7 @@ export default function BackupManagementCard() {
|
|||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const latest = backups[0];
|
||||
const setSchedule = (key, value) => setSettings(prev => ({ ...prev, [key]: value }));
|
||||
const setSchedule = (key: keyof BackupSettings, value: string | boolean) => setSettings(prev => ({ ...prev, [key]: value }));
|
||||
|
||||
async function handleCreate() {
|
||||
setBusy('create');
|
||||
|
|
@ -64,16 +82,16 @@ export default function BackupManagementCard() {
|
|||
toast.success('Backup created.');
|
||||
await load();
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to create backup.');
|
||||
toast.error(errMessage(err, 'Failed to create backup.'));
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDownload(backup) {
|
||||
async function handleDownload(backup: Backup) {
|
||||
setBusy(`download:${backup.id}`);
|
||||
try {
|
||||
const { blob, filename } = await api.downloadAdminBackup(backup.id);
|
||||
const { blob, filename } = await api.downloadAdminBackup(backup.id) as { blob: Blob; filename?: string };
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
|
|
@ -83,13 +101,13 @@ export default function BackupManagementCard() {
|
|||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to download backup.');
|
||||
toast.error(errMessage(err, 'Failed to download backup.'));
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleImport(e) {
|
||||
async function handleImport(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const file = e.target.files?.[0];
|
||||
e.target.value = '';
|
||||
if (!file) return;
|
||||
|
|
@ -100,7 +118,7 @@ export default function BackupManagementCard() {
|
|||
toast.success('Backup imported.');
|
||||
await load();
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to import backup.');
|
||||
toast.error(errMessage(err, 'Failed to import backup.'));
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
|
|
@ -110,12 +128,12 @@ export default function BackupManagementCard() {
|
|||
if (!restoreTarget) return;
|
||||
setBusy(`restore:${restoreTarget.id}`);
|
||||
try {
|
||||
const result = await api.restoreAdminBackup(restoreTarget.id);
|
||||
const result = await api.restoreAdminBackup(restoreTarget.id) as { pre_restore_backup?: string };
|
||||
toast.success(`Database restored. Pre-restore backup: ${result.pre_restore_backup}`);
|
||||
setRestoreTarget(null);
|
||||
await load();
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to restore backup.');
|
||||
toast.error(errMessage(err, 'Failed to restore backup.'));
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
|
|
@ -130,7 +148,7 @@ export default function BackupManagementCard() {
|
|||
setDeleteTarget(null);
|
||||
await load();
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to delete backup.');
|
||||
toast.error(errMessage(err, 'Failed to delete backup.'));
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
|
|
@ -143,12 +161,12 @@ export default function BackupManagementCard() {
|
|||
enabled: !!settings.enabled,
|
||||
frequency: settings.frequency,
|
||||
time: settings.time,
|
||||
retention_count: parseInt(settings.retention_count, 10) || 2,
|
||||
});
|
||||
retention_count: parseInt(String(settings.retention_count), 10) || 2,
|
||||
}) as Partial<BackupSettings>;
|
||||
setSettings({ ...DEFAULT_SETTINGS, ...saved });
|
||||
toast.success('Backup schedule saved.');
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to save backup schedule.');
|
||||
toast.error(errMessage(err, 'Failed to save backup schedule.'));
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
|
|
@ -161,7 +179,7 @@ export default function BackupManagementCard() {
|
|||
toast.success('Scheduled backup created.');
|
||||
await load();
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to run scheduled backup.');
|
||||
toast.error(errMessage(err, 'Failed to run scheduled backup.'));
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
|
|
@ -196,12 +214,12 @@ export default function BackupManagementCard() {
|
|||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
{[
|
||||
{([
|
||||
['Managed backups', backups.length],
|
||||
['Latest backup', latest ? formatDateTime(latest.modified_at) : '—'],
|
||||
['Latest size', latest ? fmtBytes(latest.size_bytes) : '—'],
|
||||
['Scheduled', settings.enabled ? `${settings.frequency} ${settings.time}` : 'Disabled'],
|
||||
].map(([label, value]) => (
|
||||
] as [string, React.ReactNode][]).map(([label, value]) => (
|
||||
<div key={label} className="rounded-lg border border-border bg-muted/20 p-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">{label}</p>
|
||||
<p className="text-sm font-medium mt-1 truncate">{value}</p>
|
||||
|
|
@ -258,7 +276,7 @@ export default function BackupManagementCard() {
|
|||
<td className="px-4 py-3"><BackupTypeBadge type={backup.type} /></td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{formatDateTime(backup.modified_at)}</td>
|
||||
<td className="px-4 py-3 font-mono">{fmtBytes(backup.size_bytes)}</td>
|
||||
<td className="px-4 py-3 font-mono text-xs text-muted-foreground max-w-[120px] truncate" title={backup.checksum}>
|
||||
<td className="px-4 py-3 font-mono text-xs text-muted-foreground max-w-[120px] truncate" title={backup.checksum ?? undefined}>
|
||||
{backup.checksum || '—'}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
|
|
@ -1,30 +1,50 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { AlertTriangle, Info } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/api';
|
||||
import { errMessage } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||
import { Toggle } from './adminShared';
|
||||
|
||||
function parseUtc(str) {
|
||||
interface BankSyncWorker {
|
||||
running?: boolean;
|
||||
last_run_at?: string | null;
|
||||
next_run_at?: string | null;
|
||||
}
|
||||
|
||||
interface BankSyncConfig {
|
||||
enabled?: boolean;
|
||||
sync_interval_hours?: number;
|
||||
sync_days?: number;
|
||||
debug_logging?: boolean;
|
||||
sync_days_max?: number;
|
||||
seed_days?: number;
|
||||
worker?: BankSyncWorker;
|
||||
encryption_key_source?: string;
|
||||
}
|
||||
|
||||
function parseUtc(str: string | null | undefined): Date | null {
|
||||
if (!str) return null;
|
||||
const normalized = str.includes('T') ? str : str.replace(' ', 'T') + 'Z';
|
||||
return new Date(normalized);
|
||||
}
|
||||
|
||||
function timeAgo(iso) {
|
||||
if (!iso) return null;
|
||||
const secs = Math.floor((Date.now() - parseUtc(iso).getTime()) / 1000);
|
||||
function timeAgo(iso: string | null | undefined): string | null {
|
||||
const d = parseUtc(iso);
|
||||
if (!d) return null;
|
||||
const secs = Math.floor((Date.now() - d.getTime()) / 1000);
|
||||
if (secs < 60) return 'just now';
|
||||
if (secs < 3600) return `${Math.floor(secs / 60)}m ago`;
|
||||
if (secs < 86400) return `${Math.floor(secs / 3600)}h ago`;
|
||||
return `${Math.floor(secs / 86400)}d ago`;
|
||||
}
|
||||
|
||||
function timeUntil(iso) {
|
||||
if (!iso) return null;
|
||||
const secs = Math.floor((parseUtc(iso).getTime() - Date.now()) / 1000);
|
||||
function timeUntil(iso: string | null | undefined): string | null {
|
||||
const d = parseUtc(iso);
|
||||
if (!d) return null;
|
||||
const secs = Math.floor((d.getTime() - Date.now()) / 1000);
|
||||
if (secs <= 0) return 'soon';
|
||||
if (secs < 60) return `${secs}s`;
|
||||
if (secs < 3600) return `${Math.floor(secs / 60)}m`;
|
||||
|
|
@ -32,31 +52,32 @@ function timeUntil(iso) {
|
|||
}
|
||||
|
||||
export default function BankSyncAdminCard() {
|
||||
const [config, setConfig] = useState(null);
|
||||
const [config, setConfig] = useState<BankSyncConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadError, setLoadError] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [syncInterval, setSyncInterval] = useState(4);
|
||||
const [syncDays, setSyncDays] = useState(30);
|
||||
const [syncInterval, setSyncInterval] = useState<number | string>(4);
|
||||
const [syncDays, setSyncDays] = useState<number | string>(30);
|
||||
const [debugLogging, setDebugLogging] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
api.bankSyncConfig()
|
||||
.then(d => {
|
||||
.then(raw => {
|
||||
const d = raw as BankSyncConfig;
|
||||
setConfig(d);
|
||||
setEnabled(!!d.enabled);
|
||||
setSyncInterval(d.sync_interval_hours ?? 4);
|
||||
setSyncDays(d.sync_days ?? 30);
|
||||
setDebugLogging(!!d.debug_logging);
|
||||
})
|
||||
.catch(err => setLoadError(err.message || 'Failed to load bank sync config'))
|
||||
.catch(err => setLoadError(errMessage(err, 'Failed to load bank sync config')))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
const hours = parseFloat(syncInterval);
|
||||
const days = parseInt(syncDays, 10);
|
||||
const hours = parseFloat(String(syncInterval));
|
||||
const days = parseInt(String(syncDays), 10);
|
||||
const maxDays = config?.sync_days_max ?? 45;
|
||||
if (!Number.isFinite(hours) || hours < 0.5 || hours > 168) {
|
||||
toast.error('Sync interval must be between 0.5 and 168 hours.');
|
||||
|
|
@ -68,7 +89,7 @@ export default function BankSyncAdminCard() {
|
|||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
const result = await api.setBankSyncConfig({ enabled, sync_interval_hours: hours, sync_days: days, debug_logging: debugLogging });
|
||||
const result = await api.setBankSyncConfig({ enabled, sync_interval_hours: hours, sync_days: days, debug_logging: debugLogging }) as BankSyncConfig;
|
||||
setConfig(result);
|
||||
setEnabled(!!result.enabled);
|
||||
setSyncInterval(result.sync_interval_hours ?? 4);
|
||||
|
|
@ -76,7 +97,7 @@ export default function BankSyncAdminCard() {
|
|||
setDebugLogging(!!result.debug_logging);
|
||||
toast.success('Bank sync settings saved.');
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to update bank sync setting.');
|
||||
toast.error(errMessage(err, 'Failed to update bank sync setting.'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
|
|
@ -103,8 +124,8 @@ export default function BankSyncAdminCard() {
|
|||
}
|
||||
|
||||
const changed = enabled !== !!config?.enabled
|
||||
|| parseFloat(syncInterval) !== config?.sync_interval_hours
|
||||
|| parseInt(syncDays, 10) !== config?.sync_days
|
||||
|| parseFloat(String(syncInterval)) !== config?.sync_interval_hours
|
||||
|| parseInt(String(syncDays), 10) !== config?.sync_days
|
||||
|| debugLogging !== !!config?.debug_logging;
|
||||
const worker = config?.worker;
|
||||
const seedDays = config?.seed_days ?? 44;
|
||||
|
|
@ -209,7 +230,7 @@ export default function BankSyncAdminCard() {
|
|||
</div>
|
||||
|
||||
{/* Amber warning at the SimpleFIN limit */}
|
||||
{parseInt(syncDays, 10) >= maxDays && (
|
||||
{parseInt(String(syncDays), 10) >= maxDays && (
|
||||
<div className="flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/8 px-3 py-2.5">
|
||||
<AlertTriangle className="h-3.5 w-3.5 text-amber-500 shrink-0 mt-0.5" />
|
||||
<p className="text-xs text-amber-600 dark:text-amber-400">
|
||||
Loading…
Reference in New Issue