fix(frontend): resolve UI rate limiting on Docker overview (#859)

Previously, each DockerContainerRow component made 2 API calls on mount:
- AIAPI.getSettings() for AI enabled status
- DockerMetadataAPI.getMetadata() for annotations

With 100+ containers, this resulted in 200+ API calls firing simultaneously,
exceeding the 500 requests/minute rate limit and causing 429 errors.

Fix:
- Lift AI settings check to DockerUnifiedTable parent component (1 call)
- Use pre-fetched dockerMetadata prop for annotations (already batch-fetched)
- Pass aiEnabled and initialNotes as props to child rows

This reduces API calls from O(n*2) to O(1) when loading the Docker overview.

Fixes #859
This commit is contained in:
rcourtman 2025-12-18 21:17:56 +00:00
parent ae70d9e423
commit aff4e9b814

View file

@ -772,6 +772,8 @@ const DockerContainerRow: Component<{
onCustomUrlUpdate?: (resourceId: string, url: string) => void; onCustomUrlUpdate?: (resourceId: string, url: string) => void;
showHostContext?: boolean; showHostContext?: boolean;
resourceIndentClass?: string; resourceIndentClass?: string;
aiEnabled?: boolean;
initialNotes?: string[];
}> = (props) => { }> = (props) => {
const { host, container } = props.row; const { host, container } = props.row;
const runtimeInfo = resolveHostRuntime(host); const runtimeInfo = resolveHostRuntime(host);
@ -792,26 +794,21 @@ const DockerContainerRow: Component<{
}); });
let urlInputRef: HTMLInputElement | undefined; let urlInputRef: HTMLInputElement | undefined;
// Annotations and AI state // Annotations and AI state - use props passed from parent to avoid per-row API calls
const [aiEnabled, setAiEnabled] = createSignal(false); const aiEnabled = () => props.aiEnabled ?? false;
// Check if this container is in AI context // Check if this container is in AI context
const isInAIContext = createMemo(() => aiChatStore.enabled && aiChatStore.hasContextItem(resourceId())); const isInAIContext = createMemo(() => aiChatStore.enabled && aiChatStore.hasContextItem(resourceId()));
const [annotations, setAnnotations] = createSignal<string[]>([]); // Initialize annotations from props (pre-fetched metadata) instead of per-row API call
const [annotations, setAnnotations] = createSignal<string[]>(props.initialNotes ?? []);
const [newAnnotation, setNewAnnotation] = createSignal(''); const [newAnnotation, setNewAnnotation] = createSignal('');
const [saving, setSaving] = createSignal(false); const [saving, setSaving] = createSignal(false);
// Check if AI is enabled and load annotations on mount // Update annotations if props change (e.g., parent re-fetches metadata)
createEffect(() => { createEffect(() => {
AIAPI.getSettings() const notes = props.initialNotes;
.then((settings) => setAiEnabled(settings.enabled && settings.configured)) if (notes && Array.isArray(notes)) {
.catch((err) => logger.debug('[DockerContainer] AI settings check failed:', err)); setAnnotations(notes);
}
// Load existing annotations
DockerMetadataAPI.getMetadata(resourceId())
.then((meta) => {
if (meta.notes && Array.isArray(meta.notes)) setAnnotations(meta.notes);
})
.catch((err) => logger.debug('[DockerContainer] Failed to load annotations:', err));
}); });
const saveAnnotations = async (newAnnotations: string[]) => { const saveAnnotations = async (newAnnotations: string[]) => {
@ -2387,10 +2384,21 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
// Use the breakpoint hook for responsive behavior // Use the breakpoint hook for responsive behavior
const { isMobile } = useBreakpoint(); const { isMobile } = useBreakpoint();
// AI enabled state - fetched once at the parent level to avoid per-row API calls
const [aiEnabled, setAiEnabled] = createSignal(false);
// Fetch AI settings once when component mounts
createEffect(() => {
AIAPI.getSettings()
.then((settings) => setAiEnabled(settings.enabled && settings.configured))
.catch((err) => logger.debug('[DockerUnifiedTable] AI settings check failed:', err));
});
// Caches for stable object references to prevent re-animations // Caches for stable object references to prevent re-animations
const rowCache = new Map<string, DockerRow>(); const rowCache = new Map<string, DockerRow>();
const tasksCache = new Map<string, DockerTask[]>(); const tasksCache = new Map<string, DockerTask[]>();
const tokens = createMemo(() => parseSearchTerm(props.searchTerm)); const tokens = createMemo(() => parseSearchTerm(props.searchTerm));
const [sortKey, setSortKey] = usePersistentSignal<SortKey>('dockerUnifiedSortKey', 'host', { const [sortKey, setSortKey] = usePersistentSignal<SortKey>('dockerUnifiedSortKey', 'host', {
deserialize: (value) => (SORT_KEYS.includes(value as SortKey) ? (value as SortKey) : 'host'), deserialize: (value) => (SORT_KEYS.includes(value as SortKey) ? (value as SortKey) : 'host'),
@ -2717,7 +2725,10 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
onCustomUrlUpdate={props.onCustomUrlUpdate} onCustomUrlUpdate={props.onCustomUrlUpdate}
showHostContext={!grouped} showHostContext={!grouped}
resourceIndentClass={grouped ? GROUPED_RESOURCE_INDENT : UNGROUPED_RESOURCE_INDENT} resourceIndentClass={grouped ? GROUPED_RESOURCE_INDENT : UNGROUPED_RESOURCE_INDENT}
aiEnabled={aiEnabled()}
initialNotes={metadata?.notes}
/> />
) : ( ) : (
<DockerServiceRow <DockerServiceRow
row={row} row={row}