Watch
1
0
Fork
You've already forked RedFlag
0

v0.2.6.7 — GATE-006 D, version row OSV check

- evaluateSupplyChainHold checks version row osv_status for target
  version, closing hole where we OSV-check one version but install
  another.
- confirm_dependencies uses TransitionByPackageFrom with source-state
  guard (StatusInstalling → Installed/Failed).
- Clears pinned version on successful install.
- Version bump to v0.2.6.7.
This commit is contained in:
Fimeg 2026-06-08 16:01:05 -04:00
commit 7d089ab5d2
9 changed files with 339 additions and 51 deletions

View file

@ -1,6 +1,6 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Link } from 'react-router-dom';
import { Link, useSearchParams } from 'react-router-dom';
import {
Search,
RefreshCw,
@ -47,6 +47,10 @@ const TAB_GROUPS: { key: string; label: string; statuses: string }[] = [
{ key: 'failed-ignored', label: 'Failed / Ignored', statuses: 'failed,ignored' },
];
const parseAgentUpdatesTab = (tab: string | null) => {
return TAB_GROUPS.some(group => group.key === tab) ? tab || TAB_GROUPS[0].key : TAB_GROUPS[0].key;
};
const STATUS_META: Record<string, { label: string; icon: React.ReactNode; class: string }> = {
pending: { label: 'Pending', icon: <Clock className="h-3 w-3" />, class: 'text-gray-600 bg-gray-100' },
approved: { label: 'Approved', icon: <Check className="h-3 w-3" />, class: 'text-blue-600 bg-blue-100' },
@ -59,7 +63,8 @@ const STATUS_META: Record<string, { label: string; icon: React.ReactNode; class:
};
export function AgentUpdatesEnhanced({ agentId, onNavigateToHistory }: AgentUpdatesEnhancedProps) {
const [activeTab, setActiveTab] = useState(TAB_GROUPS[0].key);
const [searchParams, setSearchParams] = useSearchParams();
const activeTab = parseAgentUpdatesTab(searchParams.get('updates_tab'));
const [currentPage, setCurrentPage] = useState(1);
const [pageSize] = useState(50);
const [searchTerm, setSearchTerm] = useState('');
@ -75,6 +80,21 @@ export function AgentUpdatesEnhanced({ agentId, onNavigateToHistory }: AgentUpda
const queryClient = useQueryClient();
const activeGroup = TAB_GROUPS.find(g => g.key === activeTab) || TAB_GROUPS[0];
useEffect(() => {
setCurrentPage(1);
setSelectedUpdates([]);
}, [activeTab]);
const selectActiveTab = (tab: string) => {
const params = new URLSearchParams(searchParams);
if (tab === TAB_GROUPS[0].key) {
params.delete('updates_tab');
} else {
params.set('updates_tab', tab);
}
setSearchParams(params, { replace: true });
};
// Fetch updates with status filter
const { data: updateData, isLoading, error, refetch } = useQuery<AgentUpdateResponse>({
queryKey: ['agent-updates', agentId, activeGroup.statuses, currentPage, pageSize, searchTerm, selectedSeverity],
@ -314,7 +334,7 @@ export function AgentUpdatesEnhanced({ agentId, onNavigateToHistory }: AgentUpda
{TAB_GROUPS.map((tab) => (
<button
key={tab.key}
onClick={() => { setActiveTab(tab.key); setCurrentPage(1); setSelectedUpdates([]); }}
onClick={() => selectActiveTab(tab.key)}
className={cn(
'px-4 py-2 border-b-2 transition-colors',
activeTab === tab.key

View file

@ -54,6 +54,32 @@ interface ChatTimelineProps {
externalSearch?: string; // external search query from parent
}
const entryPackageName = (entry: HistoryEntry): string | undefined => {
if (entry.package_name) return entry.package_name;
return typeof entry.params?.package_name === 'string' ? entry.params.package_name : undefined;
};
const entryPackageType = (entry: HistoryEntry): string | undefined => {
if (entry.package_type) return entry.package_type;
return typeof entry.params?.package_type === 'string' ? entry.params.package_type : undefined;
};
const entryUpdateLink = (entry: HistoryEntry): string | undefined => {
if (typeof entry.params?.update_id === 'string' && entry.params.update_id) {
return `/updates/${entry.params.update_id}`;
}
const packageName = entryPackageName(entry);
if (!packageName) return undefined;
const packageType = entryPackageType(entry);
if (packageType) {
return `/updates/package/${packageType}/${packageName}`;
}
return `/updates?search=${encodeURIComponent(packageName)}`;
};
// Helper function to create smart summaries for package operations
const createPackageOperationSummary = (entry: HistoryEntry): string => {
const action = entry.action.replace(/_/g, ' ');
@ -635,6 +661,8 @@ const ChatTimeline: React.FC<ChatTimelineProps> = ({ agentId, className, isScope
onToggle: () => void;
}> = ({ entry, isExpanded, isScopedView: _isScopedView, onToggle }) => {
const narrative = getNarrativeSummary(entry);
const packageName = entryPackageName(entry);
const updateLink = entryUpdateLink(entry);
return (
<div className="group rounded-lg transition-all duration-200">
@ -736,6 +764,17 @@ const ChatTimeline: React.FC<ChatTimelineProps> = ({ agentId, className, isScope
{/* Critical vitals - always visible in collapsed view */}
<div className="mt-2 ml-8 text-xs text-gray-600 space-y-1">
<div className="flex flex-wrap gap-x-4 gap-y-1">
{!_isScopedView && entry.agent_id && (
<span>
<span className="font-medium">Agent:</span>{' '}
<Link
to={`/agents/${entry.agent_id}`}
className="text-blue-700 hover:text-blue-900 hover:underline"
>
{entry.hostname || `${entry.agent_id.slice(0, 8)}...`}
</Link>
</span>
)}
<span>
<span className="font-medium">Action:</span> {entry.action.replace(/_/g, ' ')}
</span>
@ -745,9 +784,15 @@ const ChatTimeline: React.FC<ChatTimelineProps> = ({ agentId, className, isScope
<span className="text-gray-500"> (Exit Code: {entry.exit_code})</span>
)}
</span>
{entry.package_name && (
{packageName && updateLink && (
<span>
<span className="font-medium">Package:</span> {entry.package_name}
<span className="font-medium">Package:</span>{' '}
<Link
to={updateLink}
className="text-green-700 hover:text-green-900 hover:underline"
>
{packageName}
</Link>
</span>
)}
{narrative.subject && narrative.subject !== 'system operation' && narrative.subject !== entry.package_name && (
@ -793,11 +838,11 @@ const ChatTimeline: React.FC<ChatTimelineProps> = ({ agentId, className, isScope
<span className="text-gray-500 font-medium">Command ID</span>
<span className="font-mono text-gray-800 break-all">{entry.id}</span>
</div>
{entry.package_name && (
{packageName && (
<div className="flex flex-col">
<span className="text-gray-500 font-medium">Package</span>
<span className="text-gray-800 truncate" title={entry.package_name}>
{entry.package_name}
<span className="text-gray-800 truncate" title={packageName}>
{packageName}
</span>
</div>
)}
@ -1051,23 +1096,13 @@ const ChatTimeline: React.FC<ChatTimelineProps> = ({ agentId, className, isScope
{entry.hostname || 'View Agent'}
</Link>
{entry.package_name && entry.package_type && (
{packageName && updateLink && (
<Link
to={`/updates/package/${entry.package_type}/${entry.package_name}`}
to={updateLink}
className="inline-flex items-center px-2.5 py-1.5 bg-green-50 text-green-700 rounded-md hover:bg-green-100 transition-colors font-medium"
>
<Package className="h-3 w-3 mr-1" />
{entry.package_name}
</Link>
)}
{entry.package_name && !entry.package_type && (
<Link
to={`/updates?search=${encodeURIComponent(entry.package_name)}`}
className="inline-flex items-center px-2.5 py-1.5 bg-green-50 text-green-700 rounded-md hover:bg-green-100 transition-colors font-medium"
>
<Package className="h-3 w-3 mr-1" />
{entry.package_name}
{packageName}
</Link>
)}
@ -1233,4 +1268,4 @@ const ChatTimeline: React.FC<ChatTimelineProps> = ({ agentId, className, isScope
);
};
export default ChatTimeline;
export default ChatTimeline;

View file

@ -35,6 +35,11 @@ import toast from 'react-hot-toast';
import { updateApi } from '@/lib/api';
import DependencyClosureTree from '@/components/DependencyClosureTree';
type UpdatesTab = 'updates' | 'commands';
const parseUpdatesTab = (tab: string | null): UpdatesTab => {
return tab === 'commands' ? 'commands' : 'updates';
};
const Updates: React.FC = () => {
const { id } = useParams<{ id?: string }>();
@ -61,11 +66,12 @@ const Updates: React.FC = () => {
const [pendingDependencies, setPendingDependencies] = useState<string[]>([]);
const [dependencyUpdateId, setDependencyUpdateId] = useState<string | null>(null);
const [dependencyLoading, setDependencyLoading] = useState(false);
const [activeTab, setActiveTab] = useState<'updates' | 'commands'>('updates');
const activeTab = parseUpdatesTab(searchParams.get('tab'));
// Store filters in URL
useEffect(() => {
const params = new URLSearchParams();
if (activeTab !== 'updates') params.set('tab', activeTab);
if (debouncedSearchQuery) params.set('search', debouncedSearchQuery);
if (statusFilter) params.set('status', statusFilter);
if (severityFilter) params.set('severity', severityFilter);
@ -78,7 +84,17 @@ const Updates: React.FC = () => {
if (pageSize !== 100) params.set('page_size', pageSize.toString());
setSearchParams(params, { replace: true });
}, [debouncedSearchQuery, statusFilter, severityFilter, typeFilter, agentFilter, sortBy, sortOrder, currentPage, pageSize]);
}, [activeTab, debouncedSearchQuery, statusFilter, severityFilter, typeFilter, agentFilter, vulnFilter, sortBy, sortOrder, currentPage, pageSize, setSearchParams]);
const selectActiveTab = (tab: UpdatesTab) => {
const params = new URLSearchParams(searchParams);
if (tab === 'updates') {
params.delete('tab');
} else {
params.set('tab', tab);
}
setSearchParams(params, { replace: true });
};
// Fetch updates list (still used for the summary stat cards + detail fallback)
const { data: updatesData } = useUpdates({
@ -1384,7 +1400,7 @@ const Updates: React.FC = () => {
</p>
</div>
<button
onClick={() => setActiveTab('updates')}
onClick={() => selectActiveTab('updates')}
className="btn btn-ghost"
>
Back to Updates
@ -1736,7 +1752,7 @@ const Updates: React.FC = () => {
{/* Command History button */}
<button
onClick={() => setActiveTab('commands')}
onClick={() => selectActiveTab('commands')}
className="btn btn-ghost"
>
<RotateCcw className="h-4 w-4 mr-2" />
@ -2012,4 +2028,4 @@ const Updates: React.FC = () => {
);
};
export default Updates;
export default Updates;