adding ability to record logs for debug purposes

This commit is contained in:
orangecoding
2026-06-09 15:42:25 +02:00
parent 6c7d655277
commit 6bef907416
20 changed files with 2229 additions and 7 deletions

View File

@@ -30,6 +30,7 @@ import Dashboard from './views/dashboard/Dashboard.jsx';
import ListingDetail from './views/listings/ListingDetail.jsx';
import NewsModal from './components/news/NewsModal.jsx';
import { I18nProvider, availableLanguages } from './services/i18n/i18n.jsx';
import DebugLoggingBanner from './components/debug/DebugLoggingBanner.jsx';
const semiLocaleModules = import.meta.glob('/node_modules/@douyinfe/semi-ui-19/lib/es/locale/source/*.js', {
eager: true,
@@ -96,6 +97,7 @@ export default function FredyApp() {
<Layout className="app__main">
<Content className="app__content">
{versionUpdate?.newVersion && <VersionBanner />}
<DebugLoggingBanner />
{settings.demoMode && (
<>
<Banner

View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/
import { useEffect, useState } from 'react';
import { Banner } from '@douyinfe/semi-ui-19';
import { useTranslation } from '../../services/i18n/i18n.jsx';
import { fetchDebugActive } from '../../services/debugLoggingClient.js';
const POLL_INTERVAL_MS = 15000;
/**
* Persistent, non-dismissable red banner shown on every page while the admin opt-in
* "Debug Logging" feature is active. Polls the lightweight `/api/debug/active` probe
* so every authenticated user (not just admins) sees the warning, without exposing
* the rest of the settings payload.
*
* Polling interval is intentionally generous (15s) because the value only changes
* when an admin toggles the feature, which happens at human speeds. The Debug tab
* itself uses its own 3s polling for the live progress bar inside Settings.
*
* @returns {JSX.Element|null}
*/
export default function DebugLoggingBanner() {
const t = useTranslation();
const [active, setActive] = useState(false);
useEffect(() => {
let cancelled = false;
const tick = async () => {
try {
const res = await fetchDebugActive();
if (!cancelled) setActive(Boolean(res?.enabled));
} catch {
// Best-effort probe: an unauthenticated 401 (e.g. session expired) simply
// hides the banner until the next successful poll.
}
};
tick();
const id = window.setInterval(tick, POLL_INTERVAL_MS);
return () => {
cancelled = true;
window.clearInterval(id);
};
}, []);
if (!active) return null;
return (
<>
<Banner fullMode={true} type="danger" bordered closeIcon={null} description={t('app.debugLoggingBanner')} />
<br />
</>
);
}
DebugLoggingBanner.displayName = 'DebugLoggingBanner';

View File

@@ -295,6 +295,7 @@
"settings.tabExecution": "Ausführung",
"settings.tabUserSettings": "Benutzereinstellungen",
"settings.tabBackup": "Backup & Wiederherstellung",
"settings.tabDebug": "Debug",
"settings.save": "Speichern",
"settings.port": "Port",
"settings.portHelp": "Der Port, auf dem Fredy läuft.",
@@ -365,6 +366,36 @@
"settings.toastSqlitePathEmpty": "Der SQLite-Datenbankpfad darf nicht leer sein.",
"settings.toastSavedReloading": "Einstellungen erfolgreich gespeichert. Der Browser wird in 3 Sekunden neu geladen.",
"settings.toastSaveError": "Fehler beim Speichern der Einstellungen.",
"settings.debugSectionName": "Debug-Logging",
"settings.debugInfoTitle": "Was wird aufgezeichnet?",
"settings.debugInfoDescription": "Wenn aktiviert, schreibt Fredy jede Log-Zeile (debug, info, warn und error) in seine Datenbank. Maximal 5 MB werden gespeichert; sobald die Grenze erreicht ist, werden die ältesten Einträge automatisch gelöscht. Die Konsolen-Ausgabe bleibt unverändert.",
"settings.debugEnableButton": "Debug-Logging aktivieren",
"settings.debugDisableButton": "Debug-Logging deaktivieren",
"settings.debugDownloadButton": "Debug Informationen herunterladen",
"settings.debugUsedLabel": "Belegt:",
"settings.debugUsedValue": "{{used}} von {{max}} ({{percent}}%)",
"settings.debugStatusActive": "Debug-Logging ist aktiv!",
"settings.debugStatusInactive": "Debug-Logging ist inaktiv.",
"settings.debugConfirmReenableTitle": "Vorherige Logs löschen?",
"settings.debugConfirmReenableMessage": "Es sind noch Debug-Logs aus einer vorherigen Sitzung gespeichert. Möchtest du sie löschen, bevor das Debug-Logging erneut aktiviert wird?",
"settings.debugConfirmKeep": "Behalten & fortfahren",
"settings.debugConfirmDelete": "Löschen & aktivieren",
"settings.debugToastEnabled": "Debug-Logging aktiviert.",
"settings.debugToastDisabled": "Debug-Logging deaktiviert.",
"settings.debugToastEnableError": "Debug-Logging konnte nicht aktiviert werden.",
"settings.debugToastDisableError": "Debug-Logging konnte nicht deaktiviert werden.",
"settings.debugToastDownloadError": "Debug-Paket konnte nicht heruntergeladen werden.",
"settings.debugToastNoLogs": "Es sind noch keine Debug-Logs vorhanden. Aktiviere das Debug-Logging und reproduziere das Problem.",
"settings.debugClearButton": "Gespeicherte Debug-Logs löschen",
"settings.debugClearConfirmTitle": "Alle gespeicherten Debug-Logs löschen?",
"settings.debugClearConfirmMessage": "Damit werden alle gespeicherten Debug-Log-Einträge dauerhaft aus der Datenbank entfernt. Die Aufzeichnung selbst bleibt {{recordingState}}. Diese Aktion kann nicht rückgängig gemacht werden.",
"settings.debugClearConfirmRecordingOn": "Aktiv",
"settings.debugClearConfirmRecordingOff": "Inaktiv",
"settings.debugClearConfirmDelete": "Ja, Logs löschen",
"settings.debugClearConfirmCancel": "Abbrechen",
"settings.debugToastCleared": "Gespeicherte Debug-Logs wurden gelöscht.",
"settings.debugToastClearError": "Gespeicherte Debug-Logs konnten nicht gelöscht werden.",
"app.debugLoggingBanner": "Debug-Logging ist aktiv! Alles was Fredy loggt, wird in der Datenbank gespeichert. Deaktiviere es unter Einstellungen → Debug, sobald du fertig bist.",
"watchlist.sectionName": "Benachrichtigung für Watchlist",
"watchlist.sectionHelp": "Du kannst bei Änderungen an Inseraten auf deiner Watchlist benachrichtigt werden.",

View File

@@ -295,6 +295,7 @@
"settings.tabExecution": "Execution",
"settings.tabUserSettings": "User Settings",
"settings.tabBackup": "Backup & Restore",
"settings.tabDebug": "Debug",
"settings.save": "Save",
"settings.port": "Port",
"settings.portHelp": "The port on which Fredy is running.",
@@ -365,6 +366,36 @@
"settings.toastSqlitePathEmpty": "SQLite db path cannot be empty.",
"settings.toastSavedReloading": "Settings stored successfully. We will reload your browser in 3 seconds.",
"settings.toastSaveError": "Error while trying to store settings.",
"settings.debugSectionName": "Debug Logging",
"settings.debugInfoTitle": "What gets recorded?",
"settings.debugInfoDescription": "When enabled, Fredy records every log line (debug, info, warn and error) into its database. A maximum of 5 MB is kept; once the cap is reached, the oldest entries are dropped automatically. Console output is unaffected.",
"settings.debugEnableButton": "Enable debug logging",
"settings.debugDisableButton": "Disable debug logging",
"settings.debugDownloadButton": "Download debug information",
"settings.debugUsedLabel": "Used:",
"settings.debugUsedValue": "{{used}} of {{max}} ({{percent}}%)",
"settings.debugStatusActive": "Debug logging is currently active.",
"settings.debugStatusInactive": "Debug logging is currently inactive.",
"settings.debugConfirmReenableTitle": "Delete previous logs?",
"settings.debugConfirmReenableMessage": "Debug logs from a previous session are still stored. Do you want to delete them before enabling debug logging again?",
"settings.debugConfirmKeep": "Keep & continue",
"settings.debugConfirmDelete": "Delete & enable",
"settings.debugToastEnabled": "Debug logging enabled.",
"settings.debugToastDisabled": "Debug logging disabled.",
"settings.debugToastEnableError": "Could not enable debug logging.",
"settings.debugToastDisableError": "Could not disable debug logging.",
"settings.debugToastDownloadError": "Could not download the debug bundle.",
"settings.debugToastNoLogs": "No debug logs available yet. Enable debug logging first and reproduce the issue.",
"settings.debugClearButton": "Delete stored debug logs",
"settings.debugClearConfirmTitle": "Delete all stored debug logs?",
"settings.debugClearConfirmMessage": "This permanently removes every stored debug log entry from the database. Recording itself will stay {{recordingState}}. This action cannot be undone.",
"settings.debugClearConfirmRecordingOn": "ON",
"settings.debugClearConfirmRecordingOff": "OFF",
"settings.debugClearConfirmDelete": "Yes, delete logs",
"settings.debugClearConfirmCancel": "Cancel",
"settings.debugToastCleared": "Stored debug logs were deleted.",
"settings.debugToastClearError": "Could not delete the stored debug logs.",
"app.debugLoggingBanner": "Debug logging is active! Everything Fredy logs is being stored in its database. Disable it in Settings → Debug once you're done.",
"watchlist.sectionName": "Notification for Watch List",
"watchlist.sectionHelp": "You can get notified for changes on listings from your watch list.",

View File

@@ -0,0 +1,124 @@
/*
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/
/**
* Tiny client wrapping the /api/admin/debug endpoints.
*
* The server returns the same status payload from every mutation endpoint so the UI
* does not need to re-fetch after enable/disable, it can apply the response payload
* directly.
*/
function extractFileNameFromDisposition(disposition) {
const dispo = disposition || '';
// RFC 6266 says the UTF-8 encoded `filename*=` form takes precedence over the
// legacy `filename=` form when both are present. Match each form independently
// and prefer the UTF-8 one so we cannot accidentally pick the wrong encoding.
const utf8Match = dispo.match(/filename\*=UTF-8''([^;]+)/);
if (utf8Match) {
try {
return decodeURIComponent(utf8Match[1]);
} catch {
// malformed percent-encoding; fall through to the legacy form
}
}
const legacyMatch = dispo.match(/filename="?([^";]+)"?/);
if (legacyMatch) return legacyMatch[1];
return 'FredyDebug.zip';
}
/**
* Fetch the current feature status. Requires admin auth.
* @returns {Promise<{enabled:boolean, size:number, max:number, hasLogs:boolean, everEnabled:boolean}>}
*/
export async function fetchDebugStatus() {
const resp = await fetch('/api/admin/debug/status', { credentials: 'include' });
if (!resp.ok) throw new Error('Failed to load debug logging status');
return resp.json();
}
/**
* Lightweight "is debug logging active right now?" probe usable by any authenticated
* user. Used by the app-wide red banner so non-admin users also see the warning. The
* payload is intentionally a single boolean, no other settings are exposed.
*
* @returns {Promise<{enabled:boolean}>}
*/
export async function fetchDebugActive() {
const resp = await fetch('/api/debug/active', { credentials: 'include' });
if (!resp.ok) throw new Error('Failed to load debug active flag');
return resp.json();
}
/**
* Enable the feature. When clearPrevious is true, existing log rows are dropped
* before the new collection starts.
* @param {{clearPrevious?:boolean}} [options]
* @returns {Promise<object>}
*/
export async function enableDebugLogging({ clearPrevious = false } = {}) {
const resp = await fetch('/api/admin/debug/enable', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ clearPrevious }),
});
if (!resp.ok) throw new Error('Failed to enable debug logging');
return resp.json();
}
/**
* Disable the feature. Existing logs remain on disk so they can still be downloaded.
* @returns {Promise<object>}
*/
export async function disableDebugLogging() {
const resp = await fetch('/api/admin/debug/disable', {
method: 'POST',
credentials: 'include',
});
if (!resp.ok) throw new Error('Failed to disable debug logging');
return resp.json();
}
/**
* Drop every stored debug log row. Does NOT change the enabled flag: if recording
* was on, it stays on and the table simply starts filling again. Returns the new
* status payload.
* @returns {Promise<object>}
*/
export async function clearDebugLogs() {
const resp = await fetch('/api/admin/debug/logs', {
method: 'DELETE',
credentials: 'include',
});
if (!resp.ok) throw new Error('Failed to clear debug logs');
return resp.json();
}
/**
* Trigger the debug bundle download. Throws when there is nothing to export (server
* returns 409 in that case) or any other non-2xx response.
* @returns {Promise<void>}
*/
export async function downloadDebugBundle() {
const resp = await fetch('/api/admin/debug/download', { credentials: 'include' });
if (resp.status === 409) {
const data = await resp.json().catch(() => ({}));
const err = new Error(data?.error || 'No debug logs available yet');
err.code = 'NO_LOGS';
throw err;
}
if (!resp.ok) throw new Error('Failed to download debug bundle');
const blob = await resp.blob();
const fileName = extractFileNameFromDisposition(resp.headers.get('Content-Disposition'));
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
}

View File

@@ -22,6 +22,7 @@ import {
Radio,
RadioGroup,
Typography,
Progress,
} from '@douyinfe/semi-ui-19';
import { InputNumber } from '@douyinfe/semi-ui-19';
import { xhrPost, xhrGet } from '../../services/xhr';
@@ -32,7 +33,14 @@ import {
precheckRestore as clientPrecheckRestore,
restore as clientRestore,
} from '../../services/backupRestoreClient';
import { IconSave, IconRefresh, IconSignal, IconHome, IconFolder } from '@douyinfe/semi-icons';
import {
fetchDebugStatus,
enableDebugLogging as apiEnableDebugLogging,
disableDebugLogging as apiDisableDebugLogging,
downloadDebugBundle,
clearDebugLogs as apiClearDebugLogs,
} from '../../services/debugLoggingClient';
import { IconSave, IconRefresh, IconSignal, IconHome, IconFolder, IconAlertTriangle } from '@douyinfe/semi-icons';
import { debounce } from '../../utils';
import Headline from '../../components/headline/Headline.jsx';
import './GeneralSettings.less';
@@ -55,6 +63,32 @@ function formatFromTBackend(time) {
return date.getTime();
}
/**
* Human-readable byte formatter used by the Debug tab's usage label.
* @param {number} bytes
* @returns {string}
*/
function formatBytes(bytes) {
if (!Number.isFinite(bytes)) return String(bytes);
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
return `${(bytes / (1024 * 1024)).toFixed(2)} MiB`;
}
/**
* Compute the integer percentage that `used` represents of `total`, clamped to [0, 100].
* @param {number} used
* @param {number} total
* @returns {number}
*/
function percentOf(used, total) {
if (!total || total <= 0) return 0;
const pct = Math.round((used / total) * 100);
if (pct < 0) return 0;
if (pct > 100) return 100;
return pct;
}
const GeneralSettings = function GeneralSettings() {
const actions = useActions();
const t = useTranslation();
@@ -79,6 +113,20 @@ const GeneralSettings = function GeneralSettings() {
const [restoreBusy, setRestoreBusy] = React.useState(false);
const [selectedRestoreFile, setSelectedRestoreFile] = React.useState(null);
// Debug-logging tab state. status is fetched on mount + polled every 3s while the
// feature is active so the progress bar reflects the live byte budget.
// debugStatusSeq monotonically increases with every applied status update so we can
// discard stale polling responses that arrive after a manual enable/disable.
const [debugStatus, setDebugStatus] = React.useState(null);
const [debugBusy, setDebugBusy] = React.useState(false);
const [debugConfirmVisible, setDebugConfirmVisible] = React.useState(false);
const [debugClearConfirmVisible, setDebugClearConfirmVisible] = React.useState(false);
const debugStatusSeqRef = React.useRef(0);
const applyDebugStatus = React.useCallback((fresh) => {
debugStatusSeqRef.current += 1;
setDebugStatus(fresh);
}, []);
// User settings state
const homeAddress = useSelector((state) => state.userSettings.settings.home_address);
const providerDetails = useSelector((state) => state.userSettings.settings.provider_details);
@@ -127,6 +175,45 @@ const GeneralSettings = function GeneralSettings() {
setListingDeleteSkipPrompt(listingDeletionPreference?.skipPrompt ?? false);
}, [listingDeletionPreference]);
// Initial debug-status load. Subsequent updates flow through applyDebugStatus()
// (called by polling + after every enable/disable action), so this effect only
// needs to fire once on mount.
useEffect(() => {
let cancelled = false;
fetchDebugStatus()
.then((s) => {
if (!cancelled) applyDebugStatus(s);
})
.catch((e) => {
// Non-fatal: tab is still usable, polling will retry.
console.error('Failed to load debug status', e);
});
return () => {
cancelled = true;
};
}, [applyDebugStatus]);
// Live polling while the feature is active so the progress bar reflects new entries
// as they are written. We intentionally do NOT poll while inactive — the size stays
// constant and there's no Banner to update. Stale poll responses (where a manual
// enable/disable bumped the sequence in the meantime) are discarded so the UI does
// not flicker back to the previous state for ~3s.
useEffect(() => {
if (!debugStatus?.enabled) return undefined;
const id = window.setInterval(async () => {
const seqAtStart = debugStatusSeqRef.current;
try {
const fresh = await fetchDebugStatus();
if (debugStatusSeqRef.current === seqAtStart) {
applyDebugStatus(fresh);
}
} catch {
// ignore transient errors; next tick will retry
}
}, 3000);
return () => window.clearInterval(id);
}, [debugStatus?.enabled, applyDebugStatus]);
const nullOrEmpty = (val) => val == null || val.length === 0;
const handleStore = async () => {
@@ -231,6 +318,89 @@ const GeneralSettings = function GeneralSettings() {
}
}, []);
// ── Debug-logging actions ────────────────────────────────────────────────────
// performEnableDebug() centralizes the actual enable call so both branches of the
// confirm dialog ("delete" vs. "keep") plus the no-confirm fast-path can share it.
const performEnableDebug = React.useCallback(
async ({ clearPrevious }) => {
setDebugBusy(true);
try {
const fresh = await apiEnableDebugLogging({ clearPrevious });
applyDebugStatus(fresh);
// Keep the global generalSettings store in sync so the app-wide red banner
// (which reads settings.debug_logging_enabled) updates immediately.
await actions.generalSettings.getGeneralSettings();
Toast.success(t('settings.debugToastEnabled'));
} catch (e) {
console.error(e);
Toast.error(t('settings.debugToastEnableError'));
} finally {
setDebugBusy(false);
setDebugConfirmVisible(false);
}
},
[actions.generalSettings, applyDebugStatus, t],
);
const handleToggleDebugLogging = React.useCallback(async () => {
// Guard against the initial-load race: if status hasn't arrived yet, ignore the
// click. The button is also disabled when debugStatus == null, this is belt &
// braces for the case where the click somehow reached the handler anyway.
if (debugStatus == null) return;
if (debugStatus.enabled) {
setDebugBusy(true);
try {
const fresh = await apiDisableDebugLogging();
applyDebugStatus(fresh);
await actions.generalSettings.getGeneralSettings();
Toast.success(t('settings.debugToastDisabled'));
} catch (e) {
console.error(e);
Toast.error(t('settings.debugToastDisableError'));
} finally {
setDebugBusy(false);
}
return;
}
// Enabling: if logs from a previous session are still around, ask first.
if (debugStatus.hasLogs) {
setDebugConfirmVisible(true);
return;
}
await performEnableDebug({ clearPrevious: false });
}, [debugStatus, performEnableDebug, actions.generalSettings, applyDebugStatus, t]);
const handleDownloadDebugBundle = React.useCallback(async () => {
try {
await downloadDebugBundle();
} catch (e) {
console.error(e);
if (e?.code === 'NO_LOGS') {
Toast.error(t('settings.debugToastNoLogs'));
} else {
Toast.error(t('settings.debugToastDownloadError'));
}
}
}, [t]);
// Deleting stored logs is a separate action from disabling the feature: the user can
// free up the rolling buffer mid-recording without turning off collection. The
// confirmation dialog makes the destructive nature explicit.
const performClearDebugLogs = React.useCallback(async () => {
setDebugBusy(true);
try {
const fresh = await apiClearDebugLogs();
applyDebugStatus(fresh);
Toast.success(t('settings.debugToastCleared'));
} catch (e) {
console.error(e);
Toast.error(t('settings.debugToastClearError'));
} finally {
setDebugBusy(false);
setDebugClearConfirmVisible(false);
}
}, [applyDebugStatus, t]);
const handleSaveUserSettings = async () => {
try {
const responseJson = await actions.userSettings.setHomeAddress(address);
@@ -572,6 +742,98 @@ const GeneralSettings = function GeneralSettings() {
</SegmentPart>
</div>
</TabPane>
{currentUser?.isAdmin && (
<TabPane
tab={
<span>
<IconAlertTriangle
size="small"
style={{
marginRight: 6,
color: debugStatus?.enabled ? 'var(--semi-color-danger)' : undefined,
}}
/>
{t('settings.tabDebug')}
</span>
}
itemKey="debug"
>
<div className="generalSettings__tab-content">
<SegmentPart name={t('settings.debugSectionName')}>
<Banner
type="info"
fullMode={false}
closeIcon={null}
style={{ marginBottom: 12 }}
title={<div style={{ fontWeight: 600, fontSize: '14px' }}>{t('settings.debugInfoTitle')}</div>}
description={t('settings.debugInfoDescription')}
/>
{debugStatus?.enabled ? (
<Banner
type="danger"
fullMode={false}
closeIcon={null}
style={{ marginBottom: 12 }}
description={
<div>
<div style={{ fontWeight: 600 }}>{t('settings.debugStatusActive')}</div>
<div style={{ marginTop: 8 }}>
<Text type="secondary" style={{ marginRight: 8 }}>
{t('settings.debugUsedLabel')}
</Text>
<Text>
{t('settings.debugUsedValue', {
used: formatBytes(debugStatus.size),
max: formatBytes(debugStatus.max),
percent: percentOf(debugStatus.size, debugStatus.max),
})}
</Text>
<Progress
percent={percentOf(debugStatus.size, debugStatus.max)}
stroke="var(--semi-color-danger)"
aria-label="debug log storage"
style={{ marginTop: 6 }}
/>
</div>
</div>
}
/>
) : (
<div style={{ marginBottom: 12 }}>
<Text type="secondary">{t('settings.debugStatusInactive')}</Text>
</div>
)}
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
<Button
theme="solid"
type={debugStatus?.enabled ? 'danger' : 'primary'}
loading={debugBusy}
disabled={debugStatus == null}
onClick={handleToggleDebugLogging}
>
{debugStatus?.enabled ? t('settings.debugDisableButton') : t('settings.debugEnableButton')}
</Button>
<Button
theme="light"
icon={<IconSave />}
disabled={debugStatus == null || !debugStatus?.everEnabled || !debugStatus?.hasLogs}
onClick={handleDownloadDebugBundle}
>
{t('settings.debugDownloadButton')}
</Button>
{debugStatus?.hasLogs && (
<Button theme="solid" type="warning" onClick={() => setDebugClearConfirmVisible(true)}>
{t('settings.debugClearButton')}
</Button>
)}
</div>
</SegmentPart>
</div>
</TabPane>
)}
</Tabs>
</>
)}
@@ -621,6 +883,65 @@ const GeneralSettings = function GeneralSettings() {
</div>
</Modal>
)}
{debugConfirmVisible && (
<Modal
title={t('settings.debugConfirmReenableTitle')}
visible={debugConfirmVisible}
onCancel={() => {
// Defensive reset in case a network blip left debugBusy stuck while the
// user dismissed the dialog via the X / backdrop.
setDebugBusy(false);
setDebugConfirmVisible(false);
}}
footer={
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
<Button onClick={() => performEnableDebug({ clearPrevious: false })} loading={debugBusy}>
{t('settings.debugConfirmKeep')}
</Button>
<Button
type="danger"
theme="solid"
onClick={() => performEnableDebug({ clearPrevious: true })}
loading={debugBusy}
>
{t('settings.debugConfirmDelete')}
</Button>
</div>
}
>
<div>{t('settings.debugConfirmReenableMessage')}</div>
</Modal>
)}
{debugClearConfirmVisible && (
<Modal
title={t('settings.debugClearConfirmTitle')}
visible={debugClearConfirmVisible}
onCancel={() => {
setDebugBusy(false);
setDebugClearConfirmVisible(false);
}}
footer={
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
<Button onClick={() => setDebugClearConfirmVisible(false)} disabled={debugBusy}>
{t('settings.debugClearConfirmCancel')}
</Button>
<Button type="warning" theme="solid" onClick={performClearDebugLogs} loading={debugBusy}>
{t('settings.debugClearConfirmDelete')}
</Button>
</div>
}
>
<div>
{t('settings.debugClearConfirmMessage', {
recordingState: debugStatus?.enabled
? t('settings.debugClearConfirmRecordingOn')
: t('settings.debugClearConfirmRecordingOff'),
})}
</div>
</Modal>
)}
</div>
);
};