Skip to content

Commit e4a2bb4

Browse files
author
ScriptedAlchemy
committed
feat(dashboard): restructure curation tabs and surface LCM payload GC
The curation panel crammed scheduler, config, run controls, proposals, skills, and the oplog into one ~6800px pane behind a sub-tab named "History". Split it into Plan / Automation / Proposals / History / Activity so automation runs and approvals are discoverable; the Proposals tab badges its pending count and resolved fact proposals collapse behind a disclosure instead of rendering disabled buttons. Add a Store Maintenance card to the LCM tab exposing the previously UI-less payloads/health and payloads/gc endpoints (preview + tokened apply), and fix payload GC to report an empty run instead of an IO error when a store has no lcm-payloads directory yet.
1 parent 05be5bf commit e4a2bb4

14 files changed

Lines changed: 858 additions & 315 deletions

dashboard/holographic/src/CurationPanel.tsx

Lines changed: 58 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {
2+
Bot,
23
History,
4+
Inbox,
35
ListChecks,
46
ScrollText,
57
Wand2,
@@ -17,6 +19,8 @@ import {
1719
useCurationData,
1820
type CurationTab,
1921
} from "./curation/useCurationData";
22+
import { CurationAutomationPanel } from "./curation/CurationAutomationPanel";
23+
import { CurationProposalsPanel } from "./curation/CurationProposalsPanel";
2024
import { CurationHistoryPanel } from "./curation/CurationHistoryPanel";
2125
import { InlineConfirm } from "./curation/InlineConfirm";
2226
import { isActiveAutomationStatus, type AutomationRunTask } from "./curation/automationTasks";
@@ -198,8 +202,16 @@ export default function CurationPanel({
198202
const selectedImprovementRecommendation = selectedManagedSkill
199203
? managedSkillImprovementRecommendations[selectedManagedSkill.metadata.id]
200204
: null;
205+
const pendingProposalCount =
206+
factProposals.filter((proposal) => proposal.state === "pending_approval").length +
207+
managedSkills.filter((skill) => skill.metadata.state === "pending_approval").length;
208+
const proposalsLabel = pendingProposalCount
209+
? `Proposals ${pendingProposalCount}`
210+
: "Proposals";
201211
const tabs: Array<{ id: CurationTab; label: string; Icon: typeof Wand2 }> = [
202212
{ id: "plan", label: planLabel, Icon: ListChecks },
213+
{ id: "automation", label: "Automation", Icon: Bot },
214+
{ id: "proposals", label: proposalsLabel, Icon: Inbox },
203215
{ id: "history", label: "History", Icon: History },
204216
{ id: "activity", label: "Activity", Icon: ScrollText },
205217
];
@@ -244,7 +256,7 @@ export default function CurationPanel({
244256
ref={panelRef}
245257
role="tablist"
246258
aria-label="Curation views"
247-
className="grid grid-cols-3 gap-1 rounded-sm border border-border bg-secondary/30 p-1 shrink-0"
259+
className="grid grid-cols-5 gap-1 rounded-sm border border-border bg-secondary/30 p-1 shrink-0"
248260
>
249261
{tabs.map(({ id, label, Icon }) => {
250262
const active = activeTab === id;
@@ -406,21 +418,11 @@ export default function CurationPanel({
406418
</div>
407419
) : null}
408420

409-
{activeTab === "history" ? (
410-
<CurationHistoryPanel
411-
report={report}
412-
previewSavedAt={previewSavedAt}
413-
previewStale={previewStale}
414-
previewStaleReason={previewStaleReason}
415-
actionsLength={actions.length}
416-
actionCounts={actionCounts}
417-
diagnosticCounts={diagnosticCounts}
418-
isPlan={isPlan}
421+
{activeTab === "automation" ? (
422+
<CurationAutomationPanel
419423
status={status}
420424
statusLoading={statusLoading}
421425
statusError={statusError}
422-
oplog={oplog}
423-
oplogError={oplogError}
424426
automationRuns={automationRuns}
425427
automationRunsError={automationRunsError}
426428
automationRunActioning={automationRunActioning}
@@ -429,19 +431,6 @@ export default function CurationPanel({
429431
automationRunArtifact={automationRunArtifact}
430432
automationRunArtifactLoading={automationRunArtifactLoading}
431433
automationRunArtifactError={automationRunArtifactError}
432-
factProposals={factProposals}
433-
factProposalsLoading={factProposalsLoading}
434-
factProposalsError={factProposalsError}
435-
factProposalActioning={factProposalActioning}
436-
managedSkills={managedSkills}
437-
selectedManagedSkillId={selectedManagedSkillId}
438-
selectedManagedSkill={selectedManagedSkill}
439-
selectedUsage={selectedUsage}
440-
selectedRecommendation={selectedRecommendation}
441-
selectedImprovementRecommendation={selectedImprovementRecommendation}
442-
managedSkillsLoading={managedSkillsLoading}
443-
managedSkillsError={managedSkillsError}
444-
managedSkillActioning={managedSkillActioning}
445434
configDraft={configDraft}
446435
configLoading={configLoading}
447436
configSaving={configSaving}
@@ -461,16 +450,10 @@ export default function CurationPanel({
461450
automationTaskLabel={automationTaskLabel}
462451
taskFieldError={taskFieldError}
463452
loadStatus={loadStatus}
464-
loadOplog={loadOplog}
465453
loadAutomationRuns={loadAutomationRuns}
466454
loadSchedulerStatus={loadSchedulerStatus}
467455
loadAutomationRunArtifact={loadAutomationRunArtifact}
468-
loadFactProposals={loadFactProposals}
469-
loadManagedSkills={loadManagedSkills}
470-
loadManagedSkill={loadManagedSkill}
471456
runAutomationTask={runAutomationTask}
472-
runFactProposalAction={runFactProposalAction}
473-
runManagedSkillAction={runManagedSkillAction}
474457
setSchedulerPaused={setSchedulerPaused}
475458
updateConfigDraft={updateConfigDraft}
476459
updateConfigTaskDraft={updateConfigTaskDraft}
@@ -480,6 +463,49 @@ export default function CurationPanel({
480463
saveConfigDraft={saveConfigDraft}
481464
/>
482465
) : null}
466+
467+
{activeTab === "proposals" ? (
468+
<CurationProposalsPanel
469+
factProposals={factProposals}
470+
factProposalsLoading={factProposalsLoading}
471+
factProposalsError={factProposalsError}
472+
factProposalActioning={factProposalActioning}
473+
managedSkills={managedSkills}
474+
selectedManagedSkillId={selectedManagedSkillId}
475+
selectedManagedSkill={selectedManagedSkill}
476+
selectedUsage={selectedUsage}
477+
selectedRecommendation={selectedRecommendation}
478+
selectedImprovementRecommendation={selectedImprovementRecommendation}
479+
managedSkillsLoading={managedSkillsLoading}
480+
managedSkillsError={managedSkillsError}
481+
managedSkillActioning={managedSkillActioning}
482+
loadFactProposals={loadFactProposals}
483+
loadManagedSkills={loadManagedSkills}
484+
loadManagedSkill={loadManagedSkill}
485+
runFactProposalAction={runFactProposalAction}
486+
runManagedSkillAction={runManagedSkillAction}
487+
/>
488+
) : null}
489+
490+
{activeTab === "history" ? (
491+
<CurationHistoryPanel
492+
report={report}
493+
previewSavedAt={previewSavedAt}
494+
previewStale={previewStale}
495+
previewStaleReason={previewStaleReason}
496+
actionsLength={actions.length}
497+
actionCounts={actionCounts}
498+
diagnosticCounts={diagnosticCounts}
499+
isPlan={isPlan}
500+
status={status}
501+
statusLoading={statusLoading}
502+
statusError={statusError}
503+
oplog={oplog}
504+
oplogError={oplogError}
505+
loadStatus={loadStatus}
506+
loadOplog={loadOplog}
507+
/>
508+
) : null}
483509
</CardContent>
484510

485511
<InlineConfirm
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { Button } from "../sdk";
2+
import { Spinner } from "../Spinner";
3+
import { AutomationConfigSection } from "./AutomationConfigSection";
4+
import { AutomationRunsSection } from "./AutomationRunsSection";
5+
import { SchedulerStatusSection } from "./SchedulerStatusSection";
6+
import type { AutomationRunTask } from "./automationTasks";
7+
import type { ConfigFieldErrors, SecondsField, TaskField } from "./configTypes";
8+
import type {
9+
AutomationSchedulerStatusResponse,
10+
AutomationTaskConfig,
11+
MemoryAutomationConfig,
12+
MemoryAutomationConfigPatch,
13+
MemoryAutomationRunArtifactPayloadResponse,
14+
MemoryAutomationRunArtifactsResponse,
15+
MemoryAutomationRunRecord,
16+
MemoryCuratorStatusResponse,
17+
} from "../types";
18+
19+
interface CurationAutomationPanelProps {
20+
status: MemoryCuratorStatusResponse | null;
21+
statusLoading: boolean;
22+
statusError: string;
23+
automationRuns: MemoryAutomationRunRecord[];
24+
automationRunsError: string;
25+
automationRunActioning: AutomationRunTask | null;
26+
automationRunError: string;
27+
automationRunArtifacts: MemoryAutomationRunArtifactsResponse | null;
28+
automationRunArtifact: MemoryAutomationRunArtifactPayloadResponse | null;
29+
automationRunArtifactLoading: string | null;
30+
automationRunArtifactError: string;
31+
configDraft: MemoryAutomationConfig | null;
32+
configLoading: boolean;
33+
configSaving: boolean;
34+
configResetting: boolean;
35+
configError: string;
36+
configFieldErrors: ConfigFieldErrors;
37+
schedulerStatus: AutomationSchedulerStatusResponse | null;
38+
schedulerStatusLoading: boolean;
39+
schedulerStatusError: string;
40+
schedulerActioning: boolean;
41+
configDirty: boolean;
42+
backendUnavailable: boolean;
43+
backendUnavailableReason: string;
44+
activeAutomationStatus: (task: AutomationRunTask) => string | undefined;
45+
automationTaskCanRun: (task: AutomationRunTask) => boolean;
46+
automationTaskTitle: (task: AutomationRunTask) => string;
47+
automationTaskLabel: (task: AutomationRunTask) => string;
48+
taskFieldError: (task: AutomationRunTask, field: TaskField) => string | undefined;
49+
loadStatus: () => void;
50+
loadAutomationRuns: () => void;
51+
loadSchedulerStatus: (force?: boolean) => void;
52+
loadAutomationRunArtifact: (runId: string, kind: string) => void;
53+
runAutomationTask: (task: AutomationRunTask) => void;
54+
setSchedulerPaused: (paused: boolean) => void;
55+
updateConfigDraft: (patch: MemoryAutomationConfigPatch) => void;
56+
updateConfigTaskDraft: (task: AutomationRunTask, patch: Partial<AutomationTaskConfig>) => void;
57+
updateTaskSeconds: (task: AutomationRunTask, key: SecondsField, value: string) => void;
58+
resetConfigDraft: () => void;
59+
resetConfigToDefaults: () => Promise<void>;
60+
saveConfigDraft: () => Promise<void>;
61+
}
62+
63+
export function CurationAutomationPanel({
64+
status,
65+
statusLoading,
66+
statusError,
67+
automationRuns,
68+
automationRunsError,
69+
automationRunActioning,
70+
automationRunError,
71+
automationRunArtifacts,
72+
automationRunArtifact,
73+
automationRunArtifactLoading,
74+
automationRunArtifactError,
75+
configDraft,
76+
configLoading,
77+
configSaving,
78+
configResetting,
79+
configError,
80+
configFieldErrors,
81+
schedulerStatus,
82+
schedulerStatusLoading,
83+
schedulerStatusError,
84+
schedulerActioning,
85+
configDirty,
86+
backendUnavailable,
87+
backendUnavailableReason,
88+
activeAutomationStatus,
89+
automationTaskCanRun,
90+
automationTaskTitle,
91+
automationTaskLabel,
92+
taskFieldError,
93+
loadStatus,
94+
loadAutomationRuns,
95+
loadSchedulerStatus,
96+
loadAutomationRunArtifact,
97+
runAutomationTask,
98+
setSchedulerPaused,
99+
updateConfigDraft,
100+
updateConfigTaskDraft,
101+
updateTaskSeconds,
102+
resetConfigDraft,
103+
resetConfigToDefaults,
104+
saveConfigDraft,
105+
}: CurationAutomationPanelProps) {
106+
return (
107+
<div
108+
role="tabpanel"
109+
id="curation-panel-automation"
110+
aria-labelledby="curation-tab-automation"
111+
className="flex flex-1 min-h-0 flex-col gap-3 overflow-y-auto overflow-x-hidden pr-1"
112+
>
113+
<div className="flex min-w-0 items-center justify-between gap-2 shrink-0">
114+
<div className="min-w-0">
115+
<div className="text-xs font-medium text-foreground">Automation</div>
116+
<div className="text-[11px] text-text-tertiary">
117+
Scheduler, task config, manual runs, and the run ledger.
118+
</div>
119+
</div>
120+
<Button
121+
size="xs"
122+
ghost
123+
disabled={statusLoading}
124+
onClick={() => {
125+
loadStatus();
126+
loadSchedulerStatus(true);
127+
loadAutomationRuns();
128+
}}
129+
className="shrink-0 gap-2"
130+
>
131+
{statusLoading ? <Spinner /> : null}
132+
Refresh
133+
</Button>
134+
</div>
135+
{statusError ? (
136+
<div className="border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive shrink-0">
137+
{statusError}
138+
</div>
139+
) : null}
140+
<SchedulerStatusSection
141+
status={schedulerStatus}
142+
loading={schedulerStatusLoading}
143+
error={schedulerStatusError}
144+
actioning={schedulerActioning}
145+
onSetPaused={setSchedulerPaused}
146+
/>
147+
<AutomationConfigSection
148+
configDraft={configDraft}
149+
configLoading={configLoading}
150+
configSaving={configSaving}
151+
configResetting={configResetting}
152+
configError={configError}
153+
configFieldErrors={configFieldErrors}
154+
configDirty={configDirty}
155+
backendUnavailable={backendUnavailable}
156+
backendUnavailableReason={backendUnavailableReason}
157+
automationRunActioning={automationRunActioning}
158+
automationRunError={automationRunError}
159+
paused={Boolean(status?.state.paused)}
160+
activeAutomationStatus={activeAutomationStatus}
161+
automationTaskCanRun={automationTaskCanRun}
162+
automationTaskTitle={automationTaskTitle}
163+
automationTaskLabel={automationTaskLabel}
164+
taskFieldError={taskFieldError}
165+
runAutomationTask={runAutomationTask}
166+
updateConfigDraft={updateConfigDraft}
167+
updateConfigTaskDraft={updateConfigTaskDraft}
168+
updateTaskSeconds={updateTaskSeconds}
169+
resetConfigDraft={resetConfigDraft}
170+
resetConfigToDefaults={resetConfigToDefaults}
171+
saveConfigDraft={saveConfigDraft}
172+
/>
173+
<AutomationRunsSection
174+
runs={automationRuns}
175+
error={automationRunsError}
176+
artifacts={automationRunArtifacts}
177+
artifact={automationRunArtifact}
178+
artifactLoading={automationRunArtifactLoading}
179+
artifactError={automationRunArtifactError}
180+
onLoadArtifact={loadAutomationRunArtifact}
181+
/>
182+
</div>
183+
);
184+
}

0 commit comments

Comments
 (0)