Fix settings toggles layout and add processing-log retention controls
This commit is contained in:
@@ -31,6 +31,7 @@ Do not hardcode new palette or spacing values in component styles when a token a
|
|||||||
## Control Standards
|
## Control Standards
|
||||||
|
|
||||||
- Global input, select, textarea, and button styles are defined once in `frontend/src/styles.css`.
|
- 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.
|
- 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.
|
- 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.
|
- Focus states use `:focus-visible` and tokenized focus color to preserve keyboard discoverability.
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
DisplaySettings,
|
DisplaySettings,
|
||||||
HandwritingStyleClusteringSettings,
|
HandwritingStyleClusteringSettings,
|
||||||
OcrTaskSettings,
|
OcrTaskSettings,
|
||||||
|
ProcessingLogRetentionSettings,
|
||||||
PredefinedPathEntry,
|
PredefinedPathEntry,
|
||||||
PredefinedTagEntry,
|
PredefinedTagEntry,
|
||||||
ProviderSettings,
|
ProviderSettings,
|
||||||
@@ -47,6 +48,24 @@ function parseCardsPerPageInput(input: string, fallback: number): number {
|
|||||||
return clampCardsPerPage(parsed);
|
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.
|
* Renders compact human-oriented settings controls.
|
||||||
*/
|
*/
|
||||||
@@ -69,6 +88,7 @@ export default function SettingsScreen({
|
|||||||
const [newPredefinedTag, setNewPredefinedTag] = useState<string>('');
|
const [newPredefinedTag, setNewPredefinedTag] = useState<string>('');
|
||||||
const [uploadDefaults, setUploadDefaults] = useState<UploadDefaultsSettings | null>(null);
|
const [uploadDefaults, setUploadDefaults] = useState<UploadDefaultsSettings | null>(null);
|
||||||
const [displaySettings, setDisplaySettings] = useState<DisplaySettings | null>(null);
|
const [displaySettings, setDisplaySettings] = useState<DisplaySettings | null>(null);
|
||||||
|
const [processingLogRetention, setProcessingLogRetention] = useState<ProcessingLogRetentionSettings | null>(null);
|
||||||
const [cardsPerPageInput, setCardsPerPageInput] = useState<string>('12');
|
const [cardsPerPageInput, setCardsPerPageInput] = useState<string>('12');
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -92,6 +112,15 @@ export default function SettingsScreen({
|
|||||||
setPredefinedTags(settings.predefined_tags);
|
setPredefinedTags(settings.predefined_tags);
|
||||||
setUploadDefaults(settings.upload_defaults);
|
setUploadDefaults(settings.upload_defaults);
|
||||||
setDisplaySettings(settings.display);
|
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));
|
setCardsPerPageInput(String(settings.display.cards_per_page));
|
||||||
setError(null);
|
setError(null);
|
||||||
}, [settings]);
|
}, [settings]);
|
||||||
@@ -163,7 +192,15 @@ export default function SettingsScreen({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = useCallback(async (): Promise<void> => {
|
const handleSave = useCallback(async (): Promise<void> => {
|
||||||
if (!ocrTask || !summaryTask || !routingTask || !handwritingStyle || !uploadDefaults || !displaySettings) {
|
if (
|
||||||
|
!ocrTask ||
|
||||||
|
!summaryTask ||
|
||||||
|
!routingTask ||
|
||||||
|
!handwritingStyle ||
|
||||||
|
!uploadDefaults ||
|
||||||
|
!displaySettings ||
|
||||||
|
!processingLogRetention
|
||||||
|
) {
|
||||||
setError('Settings are not fully loaded yet');
|
setError('Settings are not fully loaded yet');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -175,7 +212,12 @@ export default function SettingsScreen({
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const resolvedCardsPerPage = parseCardsPerPageInput(cardsPerPageInput, displaySettings.cards_per_page);
|
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 });
|
setDisplaySettings({ ...displaySettings, cards_per_page: resolvedCardsPerPage });
|
||||||
|
setProcessingLogRetention(resolvedProcessingLogRetention);
|
||||||
setCardsPerPageInput(String(resolvedCardsPerPage));
|
setCardsPerPageInput(String(resolvedCardsPerPage));
|
||||||
|
|
||||||
await onSave({
|
await onSave({
|
||||||
@@ -187,6 +229,7 @@ export default function SettingsScreen({
|
|||||||
cards_per_page: resolvedCardsPerPage,
|
cards_per_page: resolvedCardsPerPage,
|
||||||
log_typing_animation_enabled: displaySettings.log_typing_animation_enabled,
|
log_typing_animation_enabled: displaySettings.log_typing_animation_enabled,
|
||||||
},
|
},
|
||||||
|
processing_log_retention: resolvedProcessingLogRetention,
|
||||||
predefined_paths: predefinedPaths,
|
predefined_paths: predefinedPaths,
|
||||||
predefined_tags: predefinedTags,
|
predefined_tags: predefinedTags,
|
||||||
handwriting_style_clustering: {
|
handwriting_style_clustering: {
|
||||||
@@ -252,21 +295,51 @@ export default function SettingsScreen({
|
|||||||
routingTask,
|
routingTask,
|
||||||
summaryTask,
|
summaryTask,
|
||||||
uploadDefaults,
|
uploadDefaults,
|
||||||
|
processingLogRetention,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!onRegisterSaveAction) {
|
if (!onRegisterSaveAction) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!settings || !ocrTask || !summaryTask || !routingTask || !handwritingStyle || !uploadDefaults || !displaySettings) {
|
if (
|
||||||
|
!settings ||
|
||||||
|
!ocrTask ||
|
||||||
|
!summaryTask ||
|
||||||
|
!routingTask ||
|
||||||
|
!handwritingStyle ||
|
||||||
|
!uploadDefaults ||
|
||||||
|
!displaySettings ||
|
||||||
|
!processingLogRetention
|
||||||
|
) {
|
||||||
onRegisterSaveAction(null);
|
onRegisterSaveAction(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onRegisterSaveAction(() => handleSave());
|
onRegisterSaveAction(() => handleSave());
|
||||||
return () => onRegisterSaveAction(null);
|
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 (
|
return (
|
||||||
<section className="settings-layout">
|
<section className="settings-layout">
|
||||||
<div className="settings-card">
|
<div className="settings-card">
|
||||||
@@ -313,6 +386,42 @@ export default function SettingsScreen({
|
|||||||
onChange={(event) => setCardsPerPageInput(event.target.value)}
|
onChange={(event) => setCardsPerPageInput(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<label className="settings-field">
|
||||||
|
Keep document sessions
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={PROCESSING_LOG_SESSION_MIN}
|
||||||
|
max={PROCESSING_LOG_SESSION_MAX}
|
||||||
|
value={processingLogRetention.keep_document_sessions}
|
||||||
|
onChange={(event) => {
|
||||||
|
const nextValue = Number.parseInt(event.target.value, 10);
|
||||||
|
if (!Number.isNaN(nextValue)) {
|
||||||
|
setProcessingLogRetention({
|
||||||
|
...processingLogRetention,
|
||||||
|
keep_document_sessions: clampProcessingLogDocumentSessions(nextValue),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="settings-field">
|
||||||
|
Keep unbound entries
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={PROCESSING_LOG_UNBOUND_MIN}
|
||||||
|
max={PROCESSING_LOG_UNBOUND_MAX}
|
||||||
|
value={processingLogRetention.keep_unbound_entries}
|
||||||
|
onChange={(event) => {
|
||||||
|
const nextValue = Number.parseInt(event.target.value, 10);
|
||||||
|
if (!Number.isNaN(nextValue)) {
|
||||||
|
setProcessingLogRetention({
|
||||||
|
...processingLogRetention,
|
||||||
|
keep_unbound_entries: clampProcessingLogUnboundEntries(nextValue),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
<label className="inline-checkbox settings-checkbox-field">
|
<label className="inline-checkbox settings-checkbox-field">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -323,6 +432,9 @@ export default function SettingsScreen({
|
|||||||
/>
|
/>
|
||||||
Processing log typing animation enabled
|
Processing log typing animation enabled
|
||||||
</label>
|
</label>
|
||||||
|
<p className="small settings-helper-text">
|
||||||
|
Processing-log retention values are used by backend trim routines when pruning historical entries.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
min-height: 2rem;
|
min-height: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
input:not([type='checkbox']):not([type='radio']),
|
||||||
select,
|
select,
|
||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -86,24 +86,39 @@ textarea {
|
|||||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast), background-color var(--transition-fast);
|
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 {
|
textarea::placeholder {
|
||||||
color: #72819e;
|
color: #72819e;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:hover,
|
input:not([type='checkbox']):not([type='radio']):hover,
|
||||||
select:hover,
|
select:hover,
|
||||||
textarea:hover {
|
textarea:hover {
|
||||||
border-color: var(--color-border-strong);
|
border-color: var(--color-border-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
input:focus,
|
input:not([type='checkbox']):not([type='radio']):focus,
|
||||||
select:focus,
|
select:focus,
|
||||||
textarea:focus {
|
textarea:focus {
|
||||||
border-color: var(--color-accent);
|
border-color: var(--color-accent);
|
||||||
box-shadow: 0 0 0 2px rgba(63, 141, 255, 0.2);
|
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 {
|
select {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
padding-right: 1.9rem;
|
padding-right: 1.9rem;
|
||||||
@@ -966,12 +981,23 @@ button:disabled {
|
|||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.45rem;
|
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;
|
font-size: 0.79rem;
|
||||||
color: #cad7ed;
|
color: #cad7ed;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-checkbox input {
|
.inline-checkbox input {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-checkbox input:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-toggle {
|
.settings-toggle {
|
||||||
|
|||||||
@@ -176,6 +176,14 @@ export interface DisplaySettings {
|
|||||||
log_typing_animation_enabled: boolean;
|
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.
|
* Represents one predefined logical path and discoverability scope.
|
||||||
*/
|
*/
|
||||||
@@ -220,6 +228,7 @@ export interface TaskSettings {
|
|||||||
export interface AppSettings {
|
export interface AppSettings {
|
||||||
upload_defaults: UploadDefaultsSettings;
|
upload_defaults: UploadDefaultsSettings;
|
||||||
display: DisplaySettings;
|
display: DisplaySettings;
|
||||||
|
processing_log_retention: ProcessingLogRetentionSettings;
|
||||||
handwriting_style_clustering: HandwritingStyleClusteringSettings;
|
handwriting_style_clustering: HandwritingStyleClusteringSettings;
|
||||||
predefined_paths: PredefinedPathEntry[];
|
predefined_paths: PredefinedPathEntry[];
|
||||||
predefined_tags: PredefinedTagEntry[];
|
predefined_tags: PredefinedTagEntry[];
|
||||||
@@ -265,6 +274,14 @@ export interface DisplaySettingsUpdate {
|
|||||||
log_typing_animation_enabled?: boolean;
|
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.
|
* Represents handwriting-style clustering settings update payload.
|
||||||
*/
|
*/
|
||||||
@@ -284,6 +301,7 @@ export interface HandwritingStyleClusteringSettingsUpdate {
|
|||||||
export interface AppSettingsUpdate {
|
export interface AppSettingsUpdate {
|
||||||
upload_defaults?: UploadDefaultsSettingsUpdate;
|
upload_defaults?: UploadDefaultsSettingsUpdate;
|
||||||
display?: DisplaySettingsUpdate;
|
display?: DisplaySettingsUpdate;
|
||||||
|
processing_log_retention?: ProcessingLogRetentionSettingsUpdate;
|
||||||
handwriting_style_clustering?: HandwritingStyleClusteringSettingsUpdate;
|
handwriting_style_clustering?: HandwritingStyleClusteringSettingsUpdate;
|
||||||
predefined_paths?: PredefinedPathEntry[];
|
predefined_paths?: PredefinedPathEntry[];
|
||||||
predefined_tags?: PredefinedTagEntry[];
|
predefined_tags?: PredefinedTagEntry[];
|
||||||
|
|||||||
Reference in New Issue
Block a user