diff --git a/doc/frontend-design-foundation.md b/doc/frontend-design-foundation.md index 289df6a..0ccf488 100644 --- a/doc/frontend-design-foundation.md +++ b/doc/frontend-design-foundation.md @@ -31,6 +31,7 @@ Do not hardcode new palette or spacing values in component styles when a token a ## Control Standards - Global input, select, textarea, and button styles are defined once in `frontend/src/styles.css`. +- Checkbox and radio controls must be styled through explicit `input[type='checkbox']` and `input[type='radio']` rules, not generic text-input selectors. - Variant button classes (`secondary-action`, `active-view-button`, `warning-action`, `danger-action`) are the only approved button color routes. - Tag chips, routing pills, card chips, and icon buttons must stay within the compact radius and spacing scale. - Focus states use `:focus-visible` and tokenized focus color to preserve keyboard discoverability. diff --git a/frontend/src/components/SettingsScreen.tsx b/frontend/src/components/SettingsScreen.tsx index bebd3ed..5b27bcd 100644 --- a/frontend/src/components/SettingsScreen.tsx +++ b/frontend/src/components/SettingsScreen.tsx @@ -12,6 +12,7 @@ import type { DisplaySettings, HandwritingStyleClusteringSettings, OcrTaskSettings, + ProcessingLogRetentionSettings, PredefinedPathEntry, PredefinedTagEntry, ProviderSettings, @@ -47,6 +48,24 @@ function parseCardsPerPageInput(input: string, fallback: number): number { return clampCardsPerPage(parsed); } +const DEFAULT_PROCESSING_LOG_RETENTION: ProcessingLogRetentionSettings = { + keep_document_sessions: 2, + keep_unbound_entries: 80, +}; + +const PROCESSING_LOG_SESSION_MIN = 0; +const PROCESSING_LOG_SESSION_MAX = 20; +const PROCESSING_LOG_UNBOUND_MIN = 0; +const PROCESSING_LOG_UNBOUND_MAX = 400; + +function clampProcessingLogDocumentSessions(value: number): number { + return Math.max(PROCESSING_LOG_SESSION_MIN, Math.min(PROCESSING_LOG_SESSION_MAX, value)); +} + +function clampProcessingLogUnboundEntries(value: number): number { + return Math.max(PROCESSING_LOG_UNBOUND_MIN, Math.min(PROCESSING_LOG_UNBOUND_MAX, value)); +} + /** * Renders compact human-oriented settings controls. */ @@ -69,6 +88,7 @@ export default function SettingsScreen({ const [newPredefinedTag, setNewPredefinedTag] = useState(''); const [uploadDefaults, setUploadDefaults] = useState(null); const [displaySettings, setDisplaySettings] = useState(null); + const [processingLogRetention, setProcessingLogRetention] = useState(null); const [cardsPerPageInput, setCardsPerPageInput] = useState('12'); const [error, setError] = useState(null); @@ -92,6 +112,15 @@ export default function SettingsScreen({ setPredefinedTags(settings.predefined_tags); setUploadDefaults(settings.upload_defaults); setDisplaySettings(settings.display); + setProcessingLogRetention({ + keep_document_sessions: clampProcessingLogDocumentSessions( + settings.processing_log_retention?.keep_document_sessions ?? + DEFAULT_PROCESSING_LOG_RETENTION.keep_document_sessions, + ), + keep_unbound_entries: clampProcessingLogUnboundEntries( + settings.processing_log_retention?.keep_unbound_entries ?? DEFAULT_PROCESSING_LOG_RETENTION.keep_unbound_entries, + ), + }); setCardsPerPageInput(String(settings.display.cards_per_page)); setError(null); }, [settings]); @@ -163,7 +192,15 @@ export default function SettingsScreen({ }; const handleSave = useCallback(async (): Promise => { - if (!ocrTask || !summaryTask || !routingTask || !handwritingStyle || !uploadDefaults || !displaySettings) { + if ( + !ocrTask || + !summaryTask || + !routingTask || + !handwritingStyle || + !uploadDefaults || + !displaySettings || + !processingLogRetention + ) { setError('Settings are not fully loaded yet'); return; } @@ -175,7 +212,12 @@ export default function SettingsScreen({ setError(null); try { const resolvedCardsPerPage = parseCardsPerPageInput(cardsPerPageInput, displaySettings.cards_per_page); + const resolvedProcessingLogRetention: ProcessingLogRetentionSettings = { + keep_document_sessions: clampProcessingLogDocumentSessions(processingLogRetention.keep_document_sessions), + keep_unbound_entries: clampProcessingLogUnboundEntries(processingLogRetention.keep_unbound_entries), + }; setDisplaySettings({ ...displaySettings, cards_per_page: resolvedCardsPerPage }); + setProcessingLogRetention(resolvedProcessingLogRetention); setCardsPerPageInput(String(resolvedCardsPerPage)); await onSave({ @@ -187,6 +229,7 @@ export default function SettingsScreen({ cards_per_page: resolvedCardsPerPage, log_typing_animation_enabled: displaySettings.log_typing_animation_enabled, }, + processing_log_retention: resolvedProcessingLogRetention, predefined_paths: predefinedPaths, predefined_tags: predefinedTags, handwriting_style_clustering: { @@ -252,21 +295,51 @@ export default function SettingsScreen({ routingTask, summaryTask, uploadDefaults, + processingLogRetention, ]); useEffect(() => { if (!onRegisterSaveAction) { return; } - if (!settings || !ocrTask || !summaryTask || !routingTask || !handwritingStyle || !uploadDefaults || !displaySettings) { + if ( + !settings || + !ocrTask || + !summaryTask || + !routingTask || + !handwritingStyle || + !uploadDefaults || + !displaySettings || + !processingLogRetention + ) { onRegisterSaveAction(null); return; } onRegisterSaveAction(() => handleSave()); return () => onRegisterSaveAction(null); - }, [displaySettings, handleSave, handwritingStyle, ocrTask, onRegisterSaveAction, routingTask, settings, summaryTask, uploadDefaults]); + }, [ + displaySettings, + handleSave, + handwritingStyle, + ocrTask, + onRegisterSaveAction, + processingLogRetention, + routingTask, + settings, + summaryTask, + uploadDefaults, + ]); - if (!settings || !ocrTask || !summaryTask || !routingTask || !handwritingStyle || !uploadDefaults || !displaySettings) { + if ( + !settings || + !ocrTask || + !summaryTask || + !routingTask || + !handwritingStyle || + !uploadDefaults || + !displaySettings || + !processingLogRetention + ) { return (
@@ -313,6 +386,42 @@ export default function SettingsScreen({ onChange={(event) => setCardsPerPageInput(event.target.value)} /> + + +

+ Processing-log retention values are used by backend trim routines when pruning historical entries. +

diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 908fb56..af32913 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -74,7 +74,7 @@ min-height: 2rem; } -input, +input:not([type='checkbox']):not([type='radio']), select, textarea { width: 100%; @@ -86,24 +86,39 @@ textarea { transition: border-color var(--transition-fast), box-shadow var(--transition-fast), background-color var(--transition-fast); } -input::placeholder, +input:not([type='checkbox']):not([type='radio'])::placeholder, textarea::placeholder { color: #72819e; } -input:hover, +input:not([type='checkbox']):not([type='radio']):hover, select:hover, textarea:hover { border-color: var(--color-border-strong); } -input:focus, +input:not([type='checkbox']):not([type='radio']):focus, select:focus, textarea:focus { border-color: var(--color-accent); box-shadow: 0 0 0 2px rgba(63, 141, 255, 0.2); } +input[type='checkbox'], +input[type='radio'] { + width: 1rem; + height: 1rem; + margin: 0; + accent-color: var(--color-accent); + cursor: pointer; +} + +input[type='checkbox']:focus-visible, +input[type='radio']:focus-visible { + outline: 2px solid rgba(63, 141, 255, 0.6); + outline-offset: 2px; +} + select { appearance: none; padding-right: 1.9rem; @@ -966,12 +981,23 @@ button:disabled { grid-template-columns: auto 1fr; align-items: center; gap: 0.45rem; + min-height: 1.95rem; + padding: 0.35rem 0.45rem; + border: 1px solid rgba(70, 89, 122, 0.55); + border-radius: var(--radius-xs); + background: rgba(18, 27, 41, 0.62); font-size: 0.79rem; color: #cad7ed; + cursor: pointer; } .inline-checkbox input { margin: 0; + flex-shrink: 0; +} + +.inline-checkbox input:disabled { + cursor: not-allowed; } .settings-toggle { diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 86b6d5a..061b23b 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -176,6 +176,14 @@ export interface DisplaySettings { log_typing_animation_enabled: boolean; } +/** + * Represents retention targets used when trimming persisted processing logs. + */ +export interface ProcessingLogRetentionSettings { + keep_document_sessions: number; + keep_unbound_entries: number; +} + /** * Represents one predefined logical path and discoverability scope. */ @@ -220,6 +228,7 @@ export interface TaskSettings { export interface AppSettings { upload_defaults: UploadDefaultsSettings; display: DisplaySettings; + processing_log_retention: ProcessingLogRetentionSettings; handwriting_style_clustering: HandwritingStyleClusteringSettings; predefined_paths: PredefinedPathEntry[]; predefined_tags: PredefinedTagEntry[]; @@ -265,6 +274,14 @@ export interface DisplaySettingsUpdate { log_typing_animation_enabled?: boolean; } +/** + * Represents processing-log retention update payload. + */ +export interface ProcessingLogRetentionSettingsUpdate { + keep_document_sessions?: number; + keep_unbound_entries?: number; +} + /** * Represents handwriting-style clustering settings update payload. */ @@ -284,6 +301,7 @@ export interface HandwritingStyleClusteringSettingsUpdate { export interface AppSettingsUpdate { upload_defaults?: UploadDefaultsSettingsUpdate; display?: DisplaySettingsUpdate; + processing_log_retention?: ProcessingLogRetentionSettingsUpdate; handwriting_style_clustering?: HandwritingStyleClusteringSettingsUpdate; predefined_paths?: PredefinedPathEntry[]; predefined_tags?: PredefinedTagEntry[];