mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-13 20:27:22 +00:00
chore: merge main into fix/ui-polish, resolve StackRow conflict
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { TogglePill } from '@/components/ui/toggle-pill';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -12,6 +13,7 @@ import { SegmentedControl } from '@/components/ui/segmented-control';
|
||||
import { SettingsPrimaryButton } from './SettingsActions';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import { toast } from '@/components/ui/toast-store';
|
||||
import { SENCHO_SETTINGS_CHANGED } from '@/lib/events';
|
||||
import { useNodes } from '@/context/NodeContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { formatTimeAgo, formatTimeUntil } from '@/lib/relativeTime';
|
||||
@@ -61,6 +63,49 @@ export function UpdatesSection() {
|
||||
|
||||
const intervalMinutes = status?.intervalMinutes ?? null;
|
||||
|
||||
// Mirror activeNode.id in a ref so the PATCH handler can detect a node
|
||||
// switch mid-flight and discard a stale write.
|
||||
const activeNodeIdRef = useRef(activeNode?.id ?? null);
|
||||
activeNodeIdRef.current = activeNode?.id ?? null;
|
||||
|
||||
// Derive toggle state from the current status. When the field is missing
|
||||
// (older remote node) the toggle is disabled with a helpful message.
|
||||
const sidebarIndicators = status?.sidebarIndicators ?? false;
|
||||
const nodeSupportsSidebarSetting = status !== null && status.sidebarIndicators !== undefined;
|
||||
|
||||
const handleSidebarIndicatorsChange = useCallback(async (next: boolean) => {
|
||||
const targetNodeId = activeNodeIdRef.current;
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const res = await apiFetch('/settings', {
|
||||
method: 'PATCH',
|
||||
nodeId: targetNodeId ?? null,
|
||||
body: JSON.stringify({ image_update_sidebar_indicators: next ? '1' : '0' }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err?.error || 'Failed to update setting');
|
||||
}
|
||||
// Guard: if the active node changed while the PATCH was in flight,
|
||||
// discard the response — it belongs to a different node.
|
||||
if (activeNodeIdRef.current === targetNodeId) {
|
||||
setStatus(prev => prev ? { ...prev, sidebarIndicators: next } : prev);
|
||||
window.dispatchEvent(new CustomEvent(SENCHO_SETTINGS_CHANGED, {
|
||||
detail: { changedKeys: ['image_update_sidebar_indicators'] },
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
// Only surface the error if the active node hasn't changed. A
|
||||
// stale failure from node A must not toast while the user views
|
||||
// node B.
|
||||
if (activeNodeIdRef.current === targetNodeId) {
|
||||
toast.error((e as Error)?.message || 'Failed to update sidebar indicator setting.');
|
||||
}
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useMastheadStats(
|
||||
isLoading || intervalMinutes == null
|
||||
? null
|
||||
@@ -70,6 +115,7 @@ export function UpdatesSection() {
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const fetchStatus = async () => {
|
||||
setStatus(null);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await apiFetch('/image-updates/status');
|
||||
@@ -280,6 +326,25 @@ export function UpdatesSection() {
|
||||
</div>
|
||||
</SettingsField>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title="Sidebar" kicker="node-scoped">
|
||||
<SettingsField
|
||||
label="Show update status in sidebar"
|
||||
helper={
|
||||
status !== null && status.sidebarIndicators === undefined
|
||||
? "This node is running an older version of Sencho that does not support this setting. Upgrade the node to enable it."
|
||||
: "Show a pulsing dot when a stack has an available update and a warning icon when the check fails. The Stack Health table on the home page always shows update status regardless of this setting. Notifications are unaffected."
|
||||
}
|
||||
htmlFor="sidebar-indicators-toggle"
|
||||
>
|
||||
<TogglePill
|
||||
id="sidebar-indicators-toggle"
|
||||
checked={sidebarIndicators}
|
||||
onChange={handleSidebarIndicatorsChange}
|
||||
disabled={status === null || !nodeSupportsSidebarSetting || readOnly || isSaving}
|
||||
/>
|
||||
</SettingsField>
|
||||
</SettingsSection>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ export const SETTINGS_ITEMS: readonly SettingsItemMeta[] = [
|
||||
group: 'automation',
|
||||
label: 'Image update checks',
|
||||
description: 'How often this node polls registries to detect available image updates and raise notifications.',
|
||||
keywords: ['image', 'update', 'registry', 'check', 'interval', 'cadence', 'poll', 'auto-update', 'detection', 'recheck'],
|
||||
keywords: ['image', 'update', 'registry', 'check', 'interval', 'cadence', 'poll', 'auto-update', 'detection', 'recheck', 'sidebar', 'badge', 'dot', 'indicator', 'status'],
|
||||
tier: null,
|
||||
scope: 'node',
|
||||
},
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface PatchableSettings {
|
||||
health_gate_enabled?: '0' | '1';
|
||||
health_gate_window_seconds?: string;
|
||||
env_block_deploy_on_missing_required?: '0' | '1';
|
||||
image_update_sidebar_indicators?: '0' | '1';
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: PatchableSettings = {
|
||||
@@ -44,6 +45,7 @@ export const DEFAULT_SETTINGS: PatchableSettings = {
|
||||
health_gate_enabled: '1',
|
||||
health_gate_window_seconds: '90',
|
||||
env_block_deploy_on_missing_required: '0',
|
||||
image_update_sidebar_indicators: '1',
|
||||
};
|
||||
|
||||
export type SectionId =
|
||||
|
||||
Reference in New Issue
Block a user