diff --git a/frontend/src/core/components/tools/addStamp/StampPreviewUtils.ts b/frontend/src/core/components/tools/addStamp/StampPreviewUtils.ts index 173d18a6a..944dc3bab 100644 --- a/frontend/src/core/components/tools/addStamp/StampPreviewUtils.ts +++ b/frontend/src/core/components/tools/addStamp/StampPreviewUtils.ts @@ -1,3 +1,4 @@ +import type { CSSProperties } from 'react'; import type { AddStampParameters } from '@app/components/tools/addStamp/useAddStampParameters'; export type ContainerSize = { width: number; height: number }; @@ -48,8 +49,6 @@ export const getFirstSelectedPage = (input: string): number => { return 1; }; -export type StampPreviewStyle = { container: any; item: any }; - // Unified per-alphabet preview adjustments export type Alphabet = 'roman' | 'arabic' | 'japanese' | 'korean' | 'chinese' | 'thai'; export type AlphabetTweaks = { scale: number; rowOffsetRem: [number, number, number]; lineHeight: number; capHeightRatio: number; defaultFontSize: number }; @@ -62,11 +61,19 @@ export const ALPHABET_PREVIEW_TWEAKS: Record = { chinese: { scale: 1/1.2, rowOffsetRem: [0, 2, 2.8], lineHeight: 1, capHeightRatio: 0.72, defaultFontSize: 30 }, // temporary default font size so that it fits on the PDF thai: { scale: 1/1.2, rowOffsetRem: [-1, 0, .8], lineHeight: 1, capHeightRatio: 0.66, defaultFontSize: 80 }, }; -export const getAlphabetPreviewScale = (alphabet: string): number => (ALPHABET_PREVIEW_TWEAKS as any)[alphabet]?.scale ?? 1.0; +const getAlphabetTweaks = (alphabet: string): AlphabetTweaks | undefined => + ALPHABET_PREVIEW_TWEAKS[alphabet as Alphabet]; -export const getDefaultFontSizeForAlphabet = (alphabet: string): number => { - return (ALPHABET_PREVIEW_TWEAKS as any)[alphabet]?.defaultFontSize ?? 80; -}; +export const getAlphabetPreviewScale = (alphabet: string): number => + getAlphabetTweaks(alphabet)?.scale ?? 1.0; + +export const getDefaultFontSizeForAlphabet = (alphabet: string): number => + getAlphabetTweaks(alphabet)?.defaultFontSize ?? 80; + +export interface StampPreviewStyle { + container: CSSProperties; + item: CSSProperties; +} export function computeStampPreviewStyle( parameters: AddStampParameters, @@ -83,7 +90,7 @@ export function computeStampPreviewStyle( const heightPts = pageSize?.heightPts ?? 841.89; // A4 height at 72 DPI const scaleX = pageWidthPx / widthPts; const scaleY = pageHeightPx / heightPts; - if (pageWidthPx <= 0 || pageHeightPx <= 0) return { item: {}, container: {} } as any; + if (pageWidthPx <= 0 || pageHeightPx <= 0) return { item: {}, container: {} }; const marginPts = (widthPts + heightPts) / 2 * (marginFactorMap[parameters.customMargin] ?? 0.035); @@ -110,24 +117,15 @@ export function computeStampPreviewStyle( // Convert measured px width back to PDF points using horizontal scale widthPtsContent = measuredWidthPx / scaleX; - let adjustmentFactor = 1.0; - switch (parameters.alphabet) { - case 'roman': - adjustmentFactor = 0.90; - break; - case 'arabic': - case 'thai': - adjustmentFactor = 0.92; - break; - case 'japanese': - case 'korean': - case 'chinese': - adjustmentFactor = 0.88; - break; - default: - adjustmentFactor = 0.93; - } - widthPtsContent *= adjustmentFactor; + const adjustments: Partial> = { + roman: 0.90, + arabic: 0.92, + thai: 0.92, + japanese: 0.88, + korean: 0.88, + chinese: 0.88, + }; + widthPtsContent *= adjustments[parameters.alphabet] ?? 0.93; } } @@ -150,7 +148,7 @@ export function computeStampPreviewStyle( if (parameters.overrideX >= 0 && parameters.overrideY >= 0) return parameters.overrideY; // For text, backend positions using cap height, not full font size const heightForY = parameters.stampType === 'text' - ? heightPtsContent * ((ALPHABET_PREVIEW_TWEAKS as any)[parameters.alphabet]?.capHeightRatio ?? 0.70) + ? heightPtsContent * (getAlphabetTweaks(parameters.alphabet)?.capHeightRatio ?? 0.70) : heightPtsContent; switch (Math.floor((position - 1) / 3)) { case 0: // Top @@ -172,12 +170,11 @@ export function computeStampPreviewStyle( try { const rootFontSizePx = parseFloat(getComputedStyle(document.documentElement).fontSize || '16') || 16; const rowIndex = Math.floor((position - 1) / 3); // 0 top, 1 middle, 2 bottom - const offsets = (ALPHABET_PREVIEW_TWEAKS as any)[parameters.alphabet]?.rowOffsetRem ?? [0, 0, 0]; + const offsets = getAlphabetTweaks(parameters.alphabet)?.rowOffsetRem ?? [0, 0, 0]; const offsetRem = offsets[rowIndex] ?? 0; yPx += offsetRem * rootFontSizePx; - } catch (e) { - // no-op - console.error(e); + } catch (error) { + console.error(error); } } const widthPx = widthPtsContent * scaleX; @@ -226,7 +223,7 @@ export function computeStampPreviewStyle( display: 'flex', flexDirection: 'column', justifyContent: 'flex-start', - lineHeight: (ALPHABET_PREVIEW_TWEAKS as any)[parameters.alphabet]?.lineHeight ?? 1, + lineHeight: getAlphabetTweaks(parameters.alphabet)?.lineHeight ?? 1, alignItems, cursor: showQuickGrid ? 'default' : 'move', pointerEvents: showQuickGrid ? 'none' : 'auto', diff --git a/frontend/src/core/components/tools/automate/ToolConfigurationModal.tsx b/frontend/src/core/components/tools/automate/ToolConfigurationModal.tsx index a4b415856..3ae899289 100644 --- a/frontend/src/core/components/tools/automate/ToolConfigurationModal.tsx +++ b/frontend/src/core/components/tools/automate/ToolConfigurationModal.tsx @@ -17,15 +17,31 @@ import WarningIcon from '@mui/icons-material/Warning'; import { ToolRegistry } from '@app/data/toolsTaxonomy'; import { ToolId } from '@app/types/toolId'; import { getAvailableToExtensions } from '@app/utils/convertUtils'; +import type { AutomationParameters } from '@app/types/automation'; + +type BaseSettingsComponent = React.ComponentType<{ + parameters: AutomationParameters; + onParameterChange: (key: string, value: unknown) => void; + disabled?: boolean; +}>; + +type ConvertSettingsComponent = React.ComponentType<{ + parameters: AutomationParameters; + onParameterChange: (key: string, value: unknown) => void; + getAvailableToExtensions: typeof getAvailableToExtensions; + selectedFiles: File[]; + disabled?: boolean; +}>; + interface ToolConfigurationModalProps { opened: boolean; tool: { id: string; operation: string; name: string; - parameters?: any; + parameters?: AutomationParameters; }; - onSave: (parameters: any) => void; + onSave: (parameters: AutomationParameters) => void; onCancel: () => void; toolRegistry: Partial; } @@ -33,11 +49,11 @@ interface ToolConfigurationModalProps { export default function ToolConfigurationModal({ opened, tool, onSave, onCancel, toolRegistry }: ToolConfigurationModalProps) { const { t } = useTranslation(); - const [parameters, setParameters] = useState({}); + const [parameters, setParameters] = useState({}); // Get tool info from registry const toolInfo = toolRegistry[tool.operation as ToolId]; - const SettingsComponent = toolInfo?.automationSettings; + const SettingsComponent = toolInfo?.automationSettings as BaseSettingsComponent | ConvertSettingsComponent | undefined; // Initialize parameters from tool (which should contain defaults from registry) useEffect(() => { @@ -49,6 +65,13 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel, } }, [tool.parameters, tool.operation]); + const updateParameter = (key: string, value: unknown) => { + setParameters(prev => ({ + ...prev, + [key]: value, + })); + }; + // Render the settings component const renderToolSettings = () => { if (!SettingsComponent) { @@ -63,12 +86,11 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel, // Special handling for ConvertSettings which needs additional props if (tool.operation === 'convert') { + const ConvertComponent = SettingsComponent as ConvertSettingsComponent; return ( - { - setParameters((prev: any) => ({ ...prev, [key]: value })); - }} + onParameterChange={updateParameter} getAvailableToExtensions={getAvailableToExtensions} selectedFiles={[]} disabled={false} @@ -76,12 +98,11 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel, ); } + const GenericComponent = SettingsComponent as BaseSettingsComponent; return ( - { - setParameters((prev: any) => ({ ...prev, [key]: value })); - }} + onParameterChange={updateParameter} disabled={false} /> ); diff --git a/frontend/src/core/contexts/file/lifecycle.ts b/frontend/src/core/contexts/file/lifecycle.ts index 93feac375..28ea8d05b 100644 --- a/frontend/src/core/contexts/file/lifecycle.ts +++ b/frontend/src/core/contexts/file/lifecycle.ts @@ -2,8 +2,9 @@ * File lifecycle management - Resource cleanup and memory management */ +import type { Dispatch, MutableRefObject } from 'react'; import { FileId } from '@app/types/file'; -import { FileContextAction, StirlingFileStub, ProcessedFilePage } from '@app/types/fileContext'; +import { FileContextAction, StirlingFileStub, ProcessedFilePage, FileContextState } from '@app/types/fileContext'; const DEBUG = process.env.NODE_ENV === 'development'; @@ -16,8 +17,8 @@ export class FileLifecycleManager { private fileGenerations = new Map(); // Generation tokens to prevent stale cleanup constructor( - private filesRef: React.MutableRefObject>, - private dispatch: React.Dispatch + private filesRef: MutableRefObject>, + private dispatch: Dispatch ) {} /** @@ -34,7 +35,7 @@ export class FileLifecycleManager { /** * Clean up resources for a specific file (with stateRef access for complete cleanup) */ - cleanupFile = (fileId: FileId, stateRef?: React.MutableRefObject): void => { + cleanupFile = (fileId: FileId, stateRef?: MutableRefObject): void => { // Use comprehensive cleanup (same as removeFiles) this.cleanupAllResourcesForFile(fileId, stateRef); @@ -68,7 +69,7 @@ export class FileLifecycleManager { /** * Schedule delayed cleanup for a file with generation token to prevent stale cleanup */ - scheduleCleanup = (fileId: FileId, delay: number = 30000, stateRef?: React.MutableRefObject): void => { + scheduleCleanup = (fileId: FileId, delay: number = 30000, stateRef?: MutableRefObject): void => { // Cancel existing timer const existingTimer = this.cleanupTimers.get(fileId); if (existingTimer) { @@ -101,7 +102,7 @@ export class FileLifecycleManager { /** * Remove a file immediately with complete resource cleanup */ - removeFiles = (fileIds: FileId[], stateRef?: React.MutableRefObject): void => { + removeFiles = (fileIds: FileId[], stateRef?: MutableRefObject): void => { fileIds.forEach(fileId => { // Clean up all resources for this file this.cleanupAllResourcesForFile(fileId, stateRef); @@ -114,7 +115,7 @@ export class FileLifecycleManager { /** * Complete resource cleanup for a single file */ - private cleanupAllResourcesForFile = (fileId: FileId, stateRef?: React.MutableRefObject): void => { + private cleanupAllResourcesForFile = (fileId: FileId, stateRef?: MutableRefObject): void => { // Remove from files ref this.filesRef.current.delete(fileId); @@ -166,7 +167,7 @@ export class FileLifecycleManager { /** * Update file record with race condition guards */ - updateStirlingFileStub = (fileId: FileId, updates: Partial, stateRef?: React.MutableRefObject): void => { + updateStirlingFileStub = (fileId: FileId, updates: Partial, stateRef?: MutableRefObject): void => { // Guard against updating removed files (race condition protection) if (!this.filesRef.current.has(fileId)) { if (DEBUG) console.warn(`🗂️ Attempted to update removed file (filesRef): ${fileId}`); diff --git a/frontend/src/core/types/fileContext.ts b/frontend/src/core/types/fileContext.ts index fa8a5da45..6fea9bbe0 100644 --- a/frontend/src/core/types/fileContext.ts +++ b/frontend/src/core/types/fileContext.ts @@ -14,14 +14,15 @@ export interface ProcessedFilePage { pageNumber?: number; rotation?: number; splitBefore?: boolean; - [key: string]: any; + splitAfter?: boolean; + originalPageNumber?: number; } export interface ProcessedFileMetadata { pages: ProcessedFilePage[]; totalPages?: number; lastProcessed?: number; - [key: string]: any; + thumbnailUrl?: string; } /** @@ -81,8 +82,8 @@ export interface StirlingFile extends File { // Type guard to check if a File object has an embedded fileId export function isStirlingFile(file: File): file is StirlingFile { - return 'fileId' in file && typeof (file as any).fileId === 'string' && - 'quickKey' in file && typeof (file as any).quickKey === 'string'; + const withIds = file as Partial; + return typeof withIds.fileId === 'string' && typeof withIds.quickKey === 'string'; } // Create a StirlingFile from a regular File object @@ -125,13 +126,16 @@ export function extractFiles(files: StirlingFile[]): File[] { } // Check if an object is a File or StirlingFile (replaces instanceof File checks) -export function isFileObject(obj: any): obj is File | StirlingFile { - return obj && - typeof obj.name === 'string' && - typeof obj.size === 'number' && - typeof obj.type === 'string' && - typeof obj.lastModified === 'number' && - typeof obj.arrayBuffer === 'function'; +export function isFileObject(obj: unknown): obj is File | StirlingFile { + if (!obj || typeof obj !== 'object') return false; + const candidate = obj as Partial; + return ( + typeof candidate.name === 'string' && + typeof candidate.size === 'number' && + typeof candidate.type === 'string' && + typeof candidate.lastModified === 'number' && + typeof candidate.arrayBuffer === 'function' + ); } diff --git a/frontend/src/desktop/contexts/AppConfigContext.tsx b/frontend/src/desktop/contexts/AppConfigContext.tsx index 399ba2e62..e703d8d7a 100644 --- a/frontend/src/desktop/contexts/AppConfigContext.tsx +++ b/frontend/src/desktop/contexts/AppConfigContext.tsx @@ -1,4 +1,5 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; +import axios from 'axios'; import apiClient from '@app/services/apiClient'; // Retry configuration @@ -12,6 +13,24 @@ function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } +interface ErrorPayload { + message?: string; + error?: string; +} + +function extractErrorMessage(error: unknown, fallback: string): string { + if (axios.isAxiosError(error)) { + return error.response?.data?.message + || error.response?.data?.error + || error.message + || fallback; + } + if (error instanceof Error) { + return error.message; + } + return fallback; +} + export interface AppConfig { baseUrl?: string; contextPath?: string; @@ -80,21 +99,22 @@ export const AppConfigProvider: React.FC<{ children: React.ReactNode }> = ({ chi console.log('[AppConfig] Successfully fetched app config'); setLoading(false); return; // Success - exit function - } catch (err: any) { - const status = err?.response?.status; + } catch (error: unknown) { + const status = axios.isAxiosError(error) ? error.response?.status : undefined; // Check if we should retry (network errors or 5xx errors) const shouldRetry = (!status || status >= 500) && attempt < MAX_RETRIES; if (shouldRetry) { - console.warn(`[AppConfig] Attempt ${attempt + 1} failed (status ${status || 'network error'}):`, err.message, '- will retry...'); + const message = extractErrorMessage(error, 'Unknown error'); + console.warn(`[AppConfig] Attempt ${attempt + 1} failed (status ${status || 'network error'}):`, message, '- will retry...'); continue; } // Final attempt failed or non-retryable error (4xx) - const errorMessage = err?.response?.data?.message || err?.message || 'Unknown error occurred'; + const errorMessage = extractErrorMessage(error, 'Unknown error occurred'); setError(errorMessage); - console.error(`[AppConfig] Failed to fetch app config after ${attempt + 1} attempts:`, err); + console.error(`[AppConfig] Failed to fetch app config after ${attempt + 1} attempts:`, error); break; } } diff --git a/frontend/src/desktop/hooks/useEndpointConfig.ts b/frontend/src/desktop/hooks/useEndpointConfig.ts index f6a3864a2..6e32ae86d 100644 --- a/frontend/src/desktop/hooks/useEndpointConfig.ts +++ b/frontend/src/desktop/hooks/useEndpointConfig.ts @@ -1,10 +1,29 @@ import { useMemo, useState, useEffect } from 'react'; +import axios from 'axios'; import apiClient from '@app/services/apiClient'; interface EndpointConfig { backendUrl: string; } +interface ErrorPayload { + message?: string; + error?: string; +} + +function extractErrorMessage(error: unknown, fallback: string): string { + if (axios.isAxiosError(error)) { + return error.response?.data?.message + || error.response?.data?.error + || error.message + || fallback; + } + if (error instanceof Error) { + return error.message; + } + return fallback; +} + /** * Desktop-specific endpoint checker that hits the backend directly via axios. */ @@ -34,8 +53,8 @@ export function useEndpointEnabled(endpoint: string): { }); setEnabled(response.data); - } catch (err: any) { - const message = err?.response?.data?.message || err?.message || 'Unknown error occurred'; + } catch (error: unknown) { + const message = extractErrorMessage(error, 'Unknown error occurred'); setError(message); setEnabled(null); } finally { @@ -83,8 +102,8 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): { }); setEndpointStatus(response.data); - } catch (err: any) { - const message = err?.response?.data?.message || err?.message || 'Unknown error occurred'; + } catch (error: unknown) { + const message = extractErrorMessage(error, 'Unknown error occurred'); setError(message); const fallbackStatus = endpoints.reduce((acc, endpointName) => {