fix(lint): resolve all ESLint errors to pass CI lint step

- Replace catch (error: any) with catch (error) + (error as Error).message cast
  in EditorLayout, NodeManager, HomeDashboard, AppStoreView
- Define TemplateVolume interface in AppStoreView; replace volumes any[] with typed array
- Define MetricPoint interface in HomeDashboard; replace metrics any[] with MetricPoint[]
- Define TerminalContainer type in BashExecModal; replace as any DOM property casts
- Define NodeTestInfo interface in NodeManager; replace info: any with typed shape
- Fix DockerNetworkStats cast in EditorLayout container stats WebSocket handler
- Remove unused catch variable (e) in api.ts and other components
- Cast streamFilter onValueChange val to union type in GlobalObservabilityView
- Add eslint-disable-next-line react-refresh/only-export-components to badge.tsx,
  button.tsx, AuthContext, NodeContext, use-data-state, use-is-in-view
- Add eslint-disable-next-line react-hooks/set-state-in-effect in LogViewer
- Add /* eslint-disable */ to animate-ui third-party primitive files
This commit is contained in:
SaelixCode
2026-03-22 00:27:43 -04:00
parent 0dd72b3eb4
commit c8a54a988b
27 changed files with 94 additions and 61 deletions
+1
View File
@@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Added:** `isValidStackName`, `isValidRemoteUrl`, `isPathWithinBase` extracted to `backend/src/utils/validation.ts` for reuse and testability.
### Fixed
- **Fixed:** ESLint CI step now passes with zero errors — replaced all `any` type annotations with proper types or `unknown` casts, fixed unused catch variables, suppressed `react-refresh/only-export-components` on files that intentionally export both a component and a hook/constant (contexts, badge, button), added file-level `eslint-disable` on animate-ui third-party primitives, and added `eslint-disable-next-line react-hooks/set-state-in-effect` for the LogViewer state reset pattern.
- **Fixed:** Four empty `catch {}` blocks in `EditorLayout` (mark-all-read, delete notification, clear-all notifications, image update fetch) now surface errors via `toast.error()` instead of silently swallowing them.
- **Fixed:** `ErrorBoundary` component existed but was not connected — it now wraps the root `<App />` in `main.tsx`, catching crashes in any context provider or route component.
- **Fixed:** `AlertDialogContent` used `asChild` on `AlertDialogPrimitive.Content` with a `motion.div` wrapper — Radix internally injects a second child (`DescriptionWarning`), causing `React.Children.only` to throw and the ErrorBoundary to trigger whenever the delete-stack confirmation dialog opened. Replaced with CSS keyframe animations (`data-[state=open]:animate-in` / `data-[state=closed]:animate-out`) which don't require `asChild`.
+18 -12
View File
@@ -17,6 +17,11 @@ export interface TemplateEnv {
default?: string;
}
interface TemplateVolume {
container?: string;
bind?: string;
}
export interface Template {
type?: number;
title: string;
@@ -24,7 +29,7 @@ export interface Template {
logo?: string;
image?: string;
ports?: string[];
volumes?: any[];
volumes?: TemplateVolume[];
env?: TemplateEnv[];
categories?: string[];
github_url?: string;
@@ -68,8 +73,8 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) {
if (!res.ok) throw new Error('Failed to fetch templates');
const data = await res.json();
setTemplates(data || []);
} catch (err: any) {
toast.error(err.message || "Failed to load App Shop");
} catch (err) {
toast.error((err as Error).message || "Failed to load App Shop");
} finally {
setLoading(false);
}
@@ -95,7 +100,7 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) {
// Initialize Volumes
const initVols: Record<string, string> = {};
t.volumes?.forEach((v: any) => {
t.volumes?.forEach((v) => {
if (v.container) {
initVols[v.container] = v.bind || `./${v.container.split('/').filter(Boolean).pop() || 'data'}`;
}
@@ -146,7 +151,7 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) {
// Process Volumes
if (modifiedTemplate.volumes) {
modifiedTemplate.volumes = modifiedTemplate.volumes.map((v: any) => {
modifiedTemplate.volumes = modifiedTemplate.volumes.map((v) => {
if (v.container && volVars[v.container] !== undefined) {
return { ...v, bind: volVars[v.container] };
}
@@ -175,8 +180,8 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) {
toast.success(`${selectedTemplate?.title} deployed successfully!`);
setIsSheetOpen(false);
onDeploySuccess(stackName.trim());
} catch (err: any) {
toast.error(err.message || 'Deployment failed');
} catch (err) {
toast.error((err as Error).message || 'Deployment failed');
} finally {
setIsDeploying(false);
}
@@ -399,14 +404,15 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) {
{selectedTemplate.volumes && selectedTemplate.volumes.length > 0 && (
<div className="space-y-4 pt-4 border-t">
<h4 className="font-semibold">Volumes (Host : Container)</h4>
{selectedTemplate.volumes.map((v: any, idx: number) => {
if (!v.container) return null;
{selectedTemplate.volumes.map((v, idx: number) => {
const containerPath = v.container;
if (!containerPath) return null;
return (
<div key={idx} className="space-y-1.5">
<Label className="text-xs text-muted-foreground font-mono">Container: {v.container}</Label>
<Label className="text-xs text-muted-foreground font-mono">Container: {containerPath}</Label>
<Input
value={volVars[v.container] !== undefined ? volVars[v.container] : ''}
onChange={(e) => setVolVars(prev => ({ ...prev, [v.container]: e.target.value }))}
value={volVars[containerPath] !== undefined ? volVars[containerPath] : ''}
onChange={(e) => setVolVars(prev => ({ ...prev, [containerPath]: e.target.value }))}
placeholder={`/path/to/host/dir`}
/>
</div>
+7 -4
View File
@@ -5,6 +5,8 @@ import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import '@xterm/xterm/css/xterm.css';
type TerminalContainer = HTMLDivElement & { __resizeObserver?: ResizeObserver };
interface BashExecModalProps {
isOpen: boolean;
onClose: () => void;
@@ -191,14 +193,15 @@ export default function BashExecModal({ isOpen, onClose, containerId, containerN
resizeObserver.observe(containerEl);
// Store observer cleanup on the container element for later
(containerEl as any).__resizeObserver = resizeObserver;
(containerEl as TerminalContainer).__resizeObserver = resizeObserver;
}
return () => {
// Clean up ResizeObserver
if (terminalRef.current && (terminalRef.current as any).__resizeObserver) {
(terminalRef.current as any).__resizeObserver.disconnect();
delete (terminalRef.current as any).__resizeObserver;
const el = terminalRef.current as TerminalContainer | null;
if (el?.__resizeObserver) {
el.__resizeObserver.disconnect();
delete el.__resizeObserver;
}
};
}, [isOpen, containerId, cleanup]);
+13 -13
View File
@@ -492,7 +492,7 @@ export default function EditorLayout() {
let currentRx = 0;
let currentTx = 0;
if (data.networks) {
Object.values(data.networks).forEach((net: any) => {
Object.values(data.networks as Record<string, { rx_bytes?: number; tx_bytes?: number }>).forEach((net) => {
currentRx += net.rx_bytes || 0;
currentTx += net.tx_bytes || 0;
});
@@ -713,9 +713,9 @@ export default function EditorLayout() {
const conts = await containersRes.json();
setContainers(Array.isArray(conts) ? conts : []);
await refreshStacks(true);
} catch (error: any) {
} catch (error) {
console.error('Failed to deploy:', error);
toast.error(error.message || 'Failed to deploy stack');
toast.error((error as Error).message || 'Failed to deploy stack');
} finally {
setLoadingAction(null);
}
@@ -741,9 +741,9 @@ export default function EditorLayout() {
const conts = await containersRes.json();
setContainers(Array.isArray(conts) ? conts : []);
await refreshStacks(true);
} catch (error: any) {
} catch (error) {
console.error('Failed to stop:', error);
toast.error(error.message || 'Failed to stop stack');
toast.error((error as Error).message || 'Failed to stop stack');
} finally {
setLoadingAction(null);
}
@@ -769,9 +769,9 @@ export default function EditorLayout() {
const conts = await containersRes.json();
setContainers(Array.isArray(conts) ? conts : []);
await refreshStacks(true);
} catch (error: any) {
} catch (error) {
console.error('Failed to restart:', error);
toast.error(error.message || 'Failed to restart stack');
toast.error((error as Error).message || 'Failed to restart stack');
} finally {
setLoadingAction(null);
}
@@ -797,9 +797,9 @@ export default function EditorLayout() {
const conts = await containersRes.json();
setContainers(Array.isArray(conts) ? conts : []);
await refreshStacks(true);
} catch (error: any) {
} catch (error) {
console.error('Failed to update:', error);
toast.error(error.message || 'Failed to update stack');
toast.error((error as Error).message || 'Failed to update stack');
} finally {
setLoadingAction(null);
}
@@ -830,9 +830,9 @@ export default function EditorLayout() {
setIsEditing(false);
}
await refreshStacks();
} catch (error: any) {
} catch (error) {
console.error('Failed to delete stack:', error);
toast.error(error.message || 'Failed to delete stack');
toast.error((error as Error).message || 'Failed to delete stack');
} finally {
setLoadingAction(null);
}
@@ -860,9 +860,9 @@ export default function EditorLayout() {
await refreshStacks();
// Auto-load the new stack in the editor pane
await loadFile(stackName);
} catch (error: any) {
} catch (error) {
console.error('Failed to create stack:', error);
toast.error(error.message || 'Failed to create stack');
toast.error((error as Error).message || 'Failed to create stack');
}
};
@@ -95,7 +95,7 @@ export function GlobalObservabilityView() {
const entry: LogEntry = JSON.parse(event.data);
entry._id = ++logIdRef.current;
bufferRef.current.push(entry);
} catch (e) { /* ignore parse errors */ }
} catch { /* ignore parse errors */ }
};
eventSource.onerror = () => {
@@ -243,7 +243,7 @@ export function GlobalObservabilityView() {
</DropdownMenuContent>
</DropdownMenu>
<Select value={streamFilter} onValueChange={(val: any) => setStreamFilter(val)}>
<Select value={streamFilter} onValueChange={(val) => setStreamFilter(val as 'ALL' | 'STDOUT' | 'STDERR')}>
<SelectTrigger className="w-[110px] h-8 text-sm">
<SelectValue placeholder="Stream" />
</SelectTrigger>
+9 -3
View File
@@ -18,6 +18,12 @@ interface Stats {
inactive: number;
}
interface MetricPoint {
timestamp: number;
cpu_percent: number;
memory_mb: number;
}
interface SystemStats {
cpu: {
usage: string;
@@ -62,7 +68,7 @@ export default function HomeDashboard() {
const [newStackName, setNewStackName] = useState('');
const [stats, setStats] = useState<Stats>({ active: 0, exited: 0, total: 0, inactive: 0 });
const [systemStats, setSystemStats] = useState<SystemStats | null>(null);
const [metrics, setMetrics] = useState<any[]>([]);
const [metrics, setMetrics] = useState<MetricPoint[]>([]);
// Fetch container stats - re-runs when active node changes so stale data is cleared immediately
useEffect(() => {
@@ -194,9 +200,9 @@ export default function HomeDashboard() {
setConvertedYaml('');
setDockerRunInput('');
window.location.reload(); // Refresh to show new stack
} catch (error: any) {
} catch (error) {
console.error('Failed to create stack:', error);
toast.error(error?.message || error?.error || 'Failed to create stack');
toast.error((error as Error).message || 'Failed to create stack');
}
};
+1
View File
@@ -24,6 +24,7 @@ export function LogViewer({ containerId, containerName, isOpen, onClose }: LogVi
useEffect(() => {
if (!isOpen || !containerId) return;
// eslint-disable-next-line react-hooks/set-state-in-effect
setLogs([]);
setIsConnected(false);
+11 -11
View File
@@ -42,7 +42,7 @@ export function NodeManager() {
const [editingNodeId, setEditingNodeId] = useState<number | null>(null);
const [deletingNode, setDeletingNode] = useState<Node | null>(null);
const [testing, setTesting] = useState<number | null>(null);
const [testResult, setTestResult] = useState<{ nodeId: number; info: any } | null>(null);
const [testResult, setTestResult] = useState<{ nodeId: number; info: { serverVersion?: string; os?: string; architecture?: string; containers?: number; images?: number; cpus?: number } } | null>(null);
// Node token generation state
const [generatedToken, setGeneratedToken] = useState<string | null>(null);
@@ -84,8 +84,8 @@ export function NodeManager() {
}
await refreshNodes();
} catch (error: any) {
toast.error(error.message || 'Failed to create node');
} catch (error) {
toast.error((error as Error).message || 'Failed to create node');
}
};
@@ -105,8 +105,8 @@ export function NodeManager() {
setEditingNodeId(null);
setFormData(defaultFormData);
await refreshNodes();
} catch (error: any) {
toast.error(error.message || 'Failed to update node');
} catch (error) {
toast.error((error as Error).message || 'Failed to update node');
}
};
@@ -135,8 +135,8 @@ export function NodeManager() {
setDeleteOpen(false);
setDeletingNode(null);
await refreshNodes();
} catch (error: any) {
toast.error(error.message || 'Failed to delete node');
} catch (error) {
toast.error((error as Error).message || 'Failed to delete node');
}
};
@@ -153,8 +153,8 @@ export function NodeManager() {
toast.error(`Failed to connect: ${result.error}`);
}
await refreshNodes();
} catch (error: any) {
toast.error(error.message || 'Connection test failed');
} catch (error) {
toast.error((error as Error).message || 'Connection test failed');
} finally {
setTesting(null);
}
@@ -169,8 +169,8 @@ export function NodeManager() {
const { token } = await res.json();
setGeneratedToken(token);
toast.success('Node token generated');
} catch (error: any) {
toast.error(error.message || 'Failed to generate token');
} catch (error) {
toast.error((error as Error).message || 'Failed to generate token');
} finally {
setGeneratingToken(false);
}
+6 -6
View File
@@ -78,8 +78,8 @@ export default function ResourcesView() {
setNetworks(await networksRes.json());
setOrphans(await orphansRes.json());
setSelectedOrphans([]);
} catch (error) {
console.error('Failed to fetch data', error);
} catch (err) {
console.error('Failed to fetch data', err);
toast.error('Failed to load resources data');
} finally {
setIsLoading(false);
@@ -105,7 +105,7 @@ export default function ResourcesView() {
toast.success(`Pruned ${confirmPruneType}.`);
}
await fetchAllData();
} catch (error) {
} catch {
toast.error(`Failed to prune ${confirmPruneType}`);
} finally {
setIsActioning(false);
@@ -124,7 +124,7 @@ export default function ResourcesView() {
if (!res.ok) throw new Error();
toast.success(`Deleted ${confirmDelete.type.slice(0, -1)}`);
await fetchAllData();
} catch (error) {
} catch {
toast.error(`Failed to delete ${confirmDelete.type.slice(0, -1)}`);
} finally {
setIsActioning(false);
@@ -154,7 +154,7 @@ export default function ResourcesView() {
toast.success(`Purged ${selectedOrphans.length} ghost container(s)`);
setBulkPurgeConfirm(false);
await fetchAllData();
} catch (error) {
} catch {
toast.error('Failed to purge selected containers.');
} finally {
setIsActioning(false);
@@ -169,7 +169,7 @@ export default function ResourcesView() {
const totalReclaimable = chartData.reduce((acc, curr) => acc + curr.value, 0);
const CustomTooltip = ({ active, payload }: any) => {
const CustomTooltip = ({ active, payload }: { active?: boolean; payload?: Array<{ name: string; value: number }> }) => {
if (active && payload && payload.length) {
return (
<div className="bg-popover text-popover-foreground border rounded-lg shadow-md p-3 text-sm">
+1 -1
View File
@@ -147,7 +147,7 @@ export function StackAlertSheet({ isOpen, onClose, stackName }: StackAlertSheetP
const err = await res.json().catch(() => ({}));
toast.error(err?.error || 'Failed to delete alert rule.');
}
} catch (e) {
} catch {
toast.error('Network error. Could not reach the node.');
} finally {
setIsLoading(false);
@@ -1,3 +1,4 @@
/* eslint-disable */
'use client';
import * as React from 'react';
@@ -1,3 +1,4 @@
/* eslint-disable */
'use client';
import * as React from 'react';
@@ -1,3 +1,4 @@
/* eslint-disable */
'use client';
import * as React from 'react';
@@ -204,4 +205,4 @@ export {
type DialogDescriptionProps,
type DialogContextType,
type DialogFlipDirection,
};
};
@@ -1,3 +1,4 @@
/* eslint-disable */
'use client';
import * as React from 'react';
@@ -560,4 +561,4 @@ export {
type DropdownMenuRadioGroupProps,
type DropdownMenuContextType,
type DropdownMenuSubContextType,
};
};
@@ -1,3 +1,4 @@
/* eslint-disable */
'use client';
import * as React from 'react';
@@ -204,4 +205,4 @@ export {
type HoverCardContentProps,
type HoverCardArrowProps,
type HoverCardContextType,
};
};
@@ -1,3 +1,4 @@
/* eslint-disable */
'use client';
import * as React from 'react';
@@ -159,4 +160,4 @@ export {
type PopoverCloseProps,
type PopoverArrowProps,
type PopoverContextType,
};
};
@@ -1,3 +1,4 @@
/* eslint-disable */
'use client';
import * as React from 'react';
@@ -188,4 +189,4 @@ export {
type SheetFooterProps,
type SheetTitleProps,
type SheetDescriptionProps,
};
};
@@ -1,3 +1,4 @@
/* eslint-disable */
'use client';
import * as React from 'react';
@@ -152,4 +153,4 @@ export {
type SwitchIconProps,
type SwitchIconPosition,
type SwitchContextType,
};
};
@@ -1,3 +1,4 @@
/* eslint-disable */
'use client';
import * as React from 'react';
@@ -217,4 +218,4 @@ export {
type TooltipContentProps,
type TooltipArrowProps,
type TooltipContextType,
};
};
@@ -1,3 +1,4 @@
/* eslint-disable */
'use client';
import * as React from 'react';
@@ -350,4 +351,4 @@ function SlidingNumber({
);
}
export { SlidingNumber, type SlidingNumberProps };
export { SlidingNumber, type SlidingNumberProps };
+1
View File
@@ -33,4 +33,5 @@ function Badge({ className, variant, ...props }: BadgeProps) {
)
}
// eslint-disable-next-line react-refresh/only-export-components
export { Badge, badgeVariants }
+1
View File
@@ -54,4 +54,5 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
)
Button.displayName = "Button"
// eslint-disable-next-line react-refresh/only-export-components
export { Button, buttonVariants }
+1
View File
@@ -108,6 +108,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
);
}
// eslint-disable-next-line react-refresh/only-export-components
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
+1
View File
@@ -98,6 +98,7 @@ export function NodeProvider({ children }: { children: React.ReactNode }) {
);
}
// eslint-disable-next-line react-refresh/only-export-components
export function useNodes() {
const context = useContext(NodeContext);
if (!context) {
+1
View File
@@ -51,4 +51,5 @@ function useDataState<T extends HTMLElement = HTMLElement>(
return [value, localRef];
}
// eslint-disable-next-line react-refresh/only-export-components
export { useDataState, type DataStateValue };
+1
View File
@@ -22,4 +22,5 @@ function useIsInView<T extends HTMLElement = HTMLElement>(
return { ref: localRef, isInView };
}
// eslint-disable-next-line react-refresh/only-export-components
export { useIsInView, type UseIsInViewOptions };
+1 -1
View File
@@ -39,7 +39,7 @@ export async function apiFetch(
if (errData.error && errData.error.includes('not found') && errData.error.includes('Node')) {
window.dispatchEvent(new Event('node-not-found'));
}
} catch (e) {
} catch {
// Ignore JSON parse errors, caller handles standard 404s
}
}