feat: add orphaned app cleanup — auto-filter stale discovered apps, manual dismiss with data purge
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,8 @@ import logStyles from '../../styles/log-panel.module.css';
|
||||
import { useAgents, useAgentEvents } from '../../api/queries/agents';
|
||||
import { useApplicationLogs } from '../../api/queries/logs';
|
||||
import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands';
|
||||
import { useCatalog, useDismissApp } from '../../api/queries/catalog';
|
||||
import { useIsAdmin } from '../../auth/auth-store';
|
||||
import { useEnvironmentStore } from '../../api/environment-store';
|
||||
import type { ConfigUpdateResponse } from '../../api/queries/commands';
|
||||
import type { AgentInstance } from '../../api/types';
|
||||
@@ -103,6 +105,12 @@ export default function AgentHealth() {
|
||||
const { data: appConfig } = useApplicationConfig(appId);
|
||||
const updateConfig = useUpdateApplicationConfig();
|
||||
|
||||
const isAdmin = useIsAdmin();
|
||||
const selectedEnvForCatalog = useEnvironmentStore((s) => s.environment);
|
||||
const { data: catalogApps } = useCatalog(selectedEnvForCatalog);
|
||||
const dismissApp = useDismissApp();
|
||||
const catalogEntry = catalogApps?.find((a) => a.slug === appId);
|
||||
|
||||
const [configEditing, setConfigEditing] = useState(false);
|
||||
const [configDraft, setConfigDraft] = useState<Record<string, string | boolean>>({});
|
||||
|
||||
@@ -468,6 +476,46 @@ export default function AgentHealth() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dismiss application card — shown when app-scoped, no agents, admin */}
|
||||
{appId && agentList.length === 0 && isAdmin && (
|
||||
<div className={`${sectionStyles.section}`} style={{ marginBottom: 16, padding: '16px 20px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<strong>No agents connected</strong>
|
||||
{catalogEntry && (
|
||||
<span style={{ marginLeft: 8, color: 'var(--text-muted)', fontSize: 13 }}>
|
||||
{catalogEntry.managed ? 'Managed app' : 'Discovered app'} — {catalogEntry.exchangeCount.toLocaleString()} exchanges recorded
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
disabled={dismissApp.isPending}
|
||||
onClick={() => {
|
||||
const count = catalogEntry?.exchangeCount ?? 0;
|
||||
const ok = window.confirm(
|
||||
`Dismiss "${appId}" and permanently delete all data (${count.toLocaleString()} exchanges)?\n\nThis action cannot be undone.`
|
||||
);
|
||||
if (ok) {
|
||||
dismissApp.mutate(appId, {
|
||||
onSuccess: () => {
|
||||
toast({ title: 'Application dismissed', description: `${appId} and all associated data have been deleted`, variant: 'success' });
|
||||
navigate('/runtime');
|
||||
},
|
||||
onError: (err) => {
|
||||
toast({ title: 'Dismiss failed', description: err.message, variant: 'error', duration: 86_400_000 });
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{dismissApp.isPending ? 'Dismissing\u2026' : 'Dismiss Application'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Group cards grid */}
|
||||
<div className={isFullWidth ? styles.groupGridSingle : styles.groupGrid}>
|
||||
{groups.map((group) => (
|
||||
|
||||
Reference in New Issue
Block a user