@growthub/cli 0.14.9 → 0.14.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/callback/route.js +35 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/failure/route.js +35 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/schedule/route.js +423 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/connect/route.js +78 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/credentials/route.js +276 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/products/[productId]/resources/route.js +173 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/products/sync/route.js +347 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/sync/route.js +293 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/provider/connect/route.js +7 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/provider/sync/route.js +7 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/sync/route.js +197 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/apps/route.js +1 -1
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/patch/preflight/route.js +38 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +3 -20
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/test-api-record/route.js +3 -20
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflow/publish/route.js +407 -290
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflows/[providerId]/route.js +209 -0
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceAddOnsMarketplace.jsx +806 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryActionCard.jsx +141 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/CeoCockpit.jsx +15 -3
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +42 -5
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +5 -1
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +86 -20
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ScheduleCockpit.jsx +363 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +8 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +322 -1
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +2 -2
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/add-ons/add-ons-client.jsx +197 -0
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/add-ons/page.jsx +23 -0
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/settings-shell.jsx +1 -0
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +734 -61
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +15 -10
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/env-status.js +2 -7
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +29 -19
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +8 -4
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/schedule-cockpit-console.js +287 -0
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/scheduler-orchestration.js +449 -0
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-secrets.js +77 -0
  39. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/serverless-readiness.js +583 -0
  40. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-on-callback.js +63 -0
  41. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-on-scheduler.js +519 -0
  42. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-ons.js +957 -0
  43. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-app-readiness.js +212 -0
  44. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +607 -63
  45. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-contract-compliance.js +168 -0
  46. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +21 -0
  47. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-operator-auth.js +32 -0
  48. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-patch-impact.js +133 -0
  49. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-provenance-lineage.js +214 -0
  50. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-stale-surfaces.js +217 -0
  51. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-workflow-impact.js +170 -0
  52. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/provider.png +0 -0
  53. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/qstash.png +0 -0
  54. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/redis.png +0 -0
  55. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/search.png +0 -0
  56. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/vector.png +0 -0
  57. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/scripts/scheduler-ingress-smoke.mjs +26 -0
  58. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +6 -0
  59. package/assets/worker-kits/growthub-custom-workspace-starter-v1/skills/governed-workspace-mutation/SKILL.md +3 -1
  60. package/dist/index.js +3024 -4191
  61. package/package.json +1 -1
@@ -0,0 +1,363 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ScheduleCockpit — the governed "/schedule" operations surface inside the
5
+ * Workspace Helper sidecar (GOVERNED_COCKPIT_ENTRY_POINT_PATTERN_V1, same
6
+ * primitive class as CeoCockpit).
7
+ *
8
+ * workspace schedule cockpit =
9
+ * pure inventory (deriveScheduleCockpit)
10
+ * + causation-derived readiness (scanServerlessReadiness, via the deriver)
11
+ * + governed action buttons over the EXISTING schedule routes
12
+ *
13
+ * One universe, same workspace truth, new command entry path. The command is not
14
+ * the feature — it is the canonical entry into the governed schedule universe.
15
+ *
16
+ * Read-only with respect to config: every action hands off to an existing
17
+ * governed schedule route (install/pause/resume/readiness/uninstall) or the
18
+ * Add-ons marketplace setup path. No client-side dataModel PATCH, no new route,
19
+ * no new object type, no second compatibility check, no new visual grammar
20
+ * beyond the existing dm-* primitives.
21
+ */
22
+
23
+ import { useCallback, useEffect, useMemo, useState } from "react";
24
+ import { ArrowUpRight, Search } from "lucide-react";
25
+ import { deriveScheduleCockpit } from "@/lib/schedule-cockpit-console";
26
+
27
+ const STATE_VARIANT = {
28
+ scheduled: "ok",
29
+ paused: "pending",
30
+ ready: "active",
31
+ blocked: "fail",
32
+ drifted: "canceled",
33
+ };
34
+
35
+ const SETUP_ROUTE = "/settings/add-ons";
36
+
37
+ function openWorkflowCanvas(card) {
38
+ if (!card?.objectId || !card?.name) return;
39
+ const params = new URLSearchParams({ object: card.objectId, row: card.name, field: "orchestrationConfig" });
40
+ window.location.assign(`/workflows?${params.toString()}`);
41
+ }
42
+
43
+ function matchesFilter(card, filter) {
44
+ switch (filter) {
45
+ case "all": return true;
46
+ case "scheduled": return card.state === "scheduled" || card.state === "paused";
47
+ case "ready": return card.state === "ready";
48
+ case "blocked": return card.state === "blocked" || card.state === "drifted";
49
+ case "local": return card.locality === "local";
50
+ case "missing-secret": return (card.readiness.deltaTags || []).includes("missing-server-secret");
51
+ case "qstash": return card.provider === "QStash";
52
+ case "custom": return card.provider === "Custom";
53
+ default: return true;
54
+ }
55
+ }
56
+
57
+ // Bound the rendered list so a workspace with hundreds of workflows stays a
58
+ // tidy, scrollable list. The attention pick shows above; overflow is disclosed.
59
+ const SCHEDULE_VISIBLE_CAP = 60;
60
+
61
+ function ScheduleCard({ card, onAction, busy, onOpen, onSeed }) {
62
+ const action = card.nextAction;
63
+ const needsAgentUpgrade = (card.readiness.deltaTags || []).includes("local-agent-upgrade-required");
64
+ return (
65
+ <div
66
+ className="dm-helper-toolcall dm-swarm-card"
67
+ data-schedule-card={card.name}
68
+ data-schedule-state={card.state}
69
+ >
70
+ <div className="dm-swarm-card-head">
71
+ <span className="dm-run-console__tree-dot" data-variant={STATE_VARIANT[card.state] || "pending"} />
72
+ <span className="dm-helper-toolcall-title dm-swarm-card-title">{card.name}</span>
73
+ <button
74
+ type="button"
75
+ className="dm-btn-ghost dm-swarm-card-action"
76
+ onClick={() => onOpen(card)}
77
+ aria-label={`Open workflow canvas: ${card.name}`}
78
+ title="Open workflow"
79
+ >
80
+ <ArrowUpRight size={12} aria-hidden="true" />
81
+ </button>
82
+ </div>
83
+
84
+ <div className="dm-swarm-card-meta">
85
+ <span className="dm-run-console__hint dm-swarm-card-kind">{card.locality === "serverless" ? "Serverless" : "Local"}</span>
86
+ {card.provider && <span className="dm-run-console__hint">{card.provider}</span>}
87
+ {card.cron && <span className="dm-run-console__hint">{card.cron}</span>}
88
+ {card.region && <span className="dm-run-console__hint">{card.region}</span>}
89
+ </div>
90
+
91
+ {card.scheduleId && (
92
+ <div className="dm-swarm-card-meta">
93
+ <span className="dm-run-console__hint" title="Installed schedule id">{card.scheduleId}</span>
94
+ {card.lastRunStatus && (
95
+ <span className="dm-run-console__hint">{`Last run ${card.lastRunStatus}`}</span>
96
+ )}
97
+ </div>
98
+ )}
99
+
100
+ {card.tags.length > 0 && (
101
+ <div className="dm-schedule-tags">
102
+ {card.tags.map((tag) => (
103
+ <span
104
+ key={tag}
105
+ className="dm-schedule-tag"
106
+ data-tag-tone={
107
+ ["Blocked", "Serverless drift", "Missing secret", "Local agent upgrade required", "Last run failed"].includes(tag)
108
+ ? "alert"
109
+ : tag === "Scheduled" ? "ok" : "neutral"
110
+ }
111
+ >
112
+ {tag}
113
+ </span>
114
+ ))}
115
+ </div>
116
+ )}
117
+
118
+ {!card.readiness.ok && card.readiness.helperActions.length > 0 && (
119
+ <div className="dm-helper-stream dm-swarm-card-desc">{card.readiness.helperActions[0]}</div>
120
+ )}
121
+
122
+ <div className="dm-schedule-card-actions">
123
+ {/* Primary state action */}
124
+ {action.kind === "setup-provider" ? (
125
+ <button type="button" className="dm-btn-ghost" onClick={() => window.location.assign(SETUP_ROUTE)} disabled={busy}>
126
+ {action.label}
127
+ </button>
128
+ ) : action.kind === "schedule" || action.kind === "manage" ? (
129
+ <button type="button" className="dm-btn-ghost" onClick={() => onOpen(card)} disabled={busy}>
130
+ {action.label}
131
+ </button>
132
+ ) : (
133
+ <button type="button" className="dm-btn-ghost" onClick={() => onAction(action.kind, card)} disabled={busy}>
134
+ {action.label}
135
+ </button>
136
+ )}
137
+ {/* Always-available readiness rescan */}
138
+ <button type="button" className="dm-btn-ghost" onClick={() => onAction("readiness", card)} disabled={busy} title="Run readiness scan">
139
+ Readiness
140
+ </button>
141
+ {/* Lifecycle controls for an installed schedule */}
142
+ {card.scheduleId && card.state !== "paused" && (
143
+ <button type="button" className="dm-btn-ghost" onClick={() => onAction("pause", card)} disabled={busy}>Pause</button>
144
+ )}
145
+ {card.scheduleId && (
146
+ <button type="button" className="dm-btn-ghost" onClick={() => onAction("uninstall", card)} disabled={busy} title="Uninstall & downgrade to local">Downgrade</button>
147
+ )}
148
+ {/* A local-agent node can't run serverless — seed a governed upgrade
149
+ proposal (/swarm or API-backed runtime) rather than scheduling it. */}
150
+ {needsAgentUpgrade && typeof onSeed === "function" && (
151
+ <button
152
+ type="button"
153
+ className="dm-btn-ghost"
154
+ onClick={() => onSeed(`Upgrade the local-agent node(s) in workflow "${card.name}" to an API-backed agent/runtime so it can run serverless:`)}
155
+ disabled={busy}
156
+ title="Seed a governed agent-upgrade proposal"
157
+ >
158
+ Upgrade agent
159
+ </button>
160
+ )}
161
+ </div>
162
+ </div>
163
+ );
164
+ }
165
+
166
+ export function ScheduleCockpit({ workspaceConfig, focus, onConfigRefresh, onOpenArtifact, onSeedSwarm, onOpenSetup }) {
167
+ const [configuredEnvRefs, setConfiguredEnvRefs] = useState([]);
168
+ const [receipts, setReceipts] = useState([]);
169
+ const [activeFilter, setActiveFilter] = useState("all");
170
+ const [search, setSearch] = useState(focus?.name ? String(focus.name) : "");
171
+ const [busyCard, setBusyCard] = useState("");
172
+ const [error, setError] = useState("");
173
+ const [notice, setNotice] = useState("");
174
+
175
+ useEffect(() => {
176
+ let cancelled = false;
177
+ (async () => {
178
+ try {
179
+ const res = await fetch("/api/workspace/env-status", { cache: "no-store" });
180
+ const data = await res.json();
181
+ if (cancelled) return;
182
+ setConfiguredEnvRefs(Array.isArray(data?.configuredEnvRefs) ? data.configuredEnvRefs : []);
183
+ } catch { /* deriver still renders structure-only readiness */ }
184
+ })();
185
+ (async () => {
186
+ try {
187
+ const res = await fetch("/api/workspace/agent-outcomes");
188
+ const data = await res.json();
189
+ if (cancelled) return;
190
+ setReceipts(Array.isArray(data?.receipts) ? data.receipts : []);
191
+ } catch { /* non-fatal */ }
192
+ })();
193
+ return () => { cancelled = true; };
194
+ }, []);
195
+
196
+ const model = useMemo(
197
+ () => deriveScheduleCockpit({ workspaceConfig, configuredEnvRefs, receipts }),
198
+ [workspaceConfig, configuredEnvRefs, receipts],
199
+ );
200
+
201
+ const providerFor = useCallback((card) => ({
202
+ providerId: card.providerId || model.defaultProvider?.providerId || "upstash",
203
+ productId: card.productId || model.defaultProvider?.productId || "upstash-qstash",
204
+ }), [model.defaultProvider]);
205
+
206
+ // All scheduler actions hand off to the EXISTING governed schedule routes.
207
+ // No client-side dataModel PATCH; failures surface the receipt-backed reason.
208
+ const runAction = useCallback(async (kind, card) => {
209
+ setError("");
210
+ setNotice("");
211
+ setBusyCard(card.cardId);
212
+ const { providerId, productId } = providerFor(card);
213
+ const base = `/api/workspace/add-ons/${providerId}/schedule`;
214
+ const body = { productId, objectId: card.objectId, rowId: card.name };
215
+ try {
216
+ let res;
217
+ if (kind === "uninstall") {
218
+ res = await fetch(base, { method: "DELETE", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
219
+ } else {
220
+ res = await fetch(base, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ...body, action: kind }) });
221
+ }
222
+ const data = await res.json().catch(() => ({}));
223
+ if (kind === "readiness") {
224
+ const r = data?.readiness;
225
+ if (r) setNotice(r.ok ? `${card.name}: serverless-ready (${r.status}).` : `${card.name}: blocked — ${(r.blockingNodes?.[0]?.helperAction) || (r.deltaTags || []).join(", ")}`);
226
+ else setError(data?.error || "Readiness scan failed.");
227
+ return;
228
+ }
229
+ if (!res.ok || data?.ok === false) {
230
+ // Resume re-runs readiness server-side; a 422 means drift blocked it.
231
+ const blocked = data?.readiness?.blockingNodes?.[0]?.helperAction;
232
+ setError(blocked || data?.error || `${kind} failed (HTTP ${res.status}).`);
233
+ return;
234
+ }
235
+ setNotice(`${card.name}: ${kind} ok.`);
236
+ if (typeof onConfigRefresh === "function") onConfigRefresh();
237
+ } catch (err) {
238
+ setError(err?.message || `${kind} failed.`);
239
+ } finally {
240
+ setBusyCard("");
241
+ }
242
+ }, [providerFor, onConfigRefresh]);
243
+
244
+ const onOpen = useCallback((card) => {
245
+ // Prefer the governed artifact handoff if the host wired one; else navigate.
246
+ if (typeof onOpenArtifact === "function") {
247
+ onOpenArtifact(card.artifact);
248
+ }
249
+ openWorkflowCanvas(card);
250
+ }, [onOpenArtifact]);
251
+
252
+ // ---- empty state: no scheduler product installed ----
253
+ if (model.schedulerSetupState === "none") {
254
+ return (
255
+ <div className="dm-swarm-cockpit" data-schedule-cockpit="" data-schedule-mode="no-provider">
256
+ <div className="dm-helper-toolcall dm-swarm-card">
257
+ <div className="dm-swarm-card-head">
258
+ <span className="dm-run-console__tree-dot" data-variant="pending" />
259
+ <span className="dm-helper-toolcall-title dm-swarm-card-title">No scheduler installed yet</span>
260
+ </div>
261
+ <div className="dm-helper-stream dm-swarm-card-desc">
262
+ Install a scheduler provider to activate Serverless Schedule for workflows.
263
+ Upstash QStash/Workflow is the first-class default; a Custom scheduler plugin works too.
264
+ </div>
265
+ <div className="dm-schedule-card-actions">
266
+ <button type="button" className="dm-btn-ghost" onClick={() => window.location.assign(SETUP_ROUTE)}>
267
+ Set up scheduler
268
+ </button>
269
+ {typeof onOpenSetup === "function" && (
270
+ <button type="button" className="dm-btn-ghost" onClick={onOpenSetup}>Open setup</button>
271
+ )}
272
+ </div>
273
+ </div>
274
+ {model.workflowCards.length > 0 && (
275
+ <p className="dm-run-console__hint">
276
+ {`${model.workflowCards.length} workflow${model.workflowCards.length === 1 ? "" : "s"} in this workspace will become schedulable once a provider is installed.`}
277
+ </p>
278
+ )}
279
+ </div>
280
+ );
281
+ }
282
+
283
+ // ---- operational state: inventory ----
284
+ const text = search.trim().toLowerCase();
285
+ const filtered = model.workflowCards.filter(
286
+ (c) => matchesFilter(c, activeFilter)
287
+ && (!text || c.name.toLowerCase().includes(text) || (c.tags || []).some((t) => t.toLowerCase().includes(text))),
288
+ );
289
+ const attention = model.attention && matchesFilter(model.attention, activeFilter) && (!text || model.attention.name.toLowerCase().includes(text))
290
+ ? model.attention
291
+ : null;
292
+ const others = attention ? filtered.filter((c) => c.cardId !== attention.cardId) : filtered;
293
+ const visible = others.slice(0, SCHEDULE_VISIBLE_CAP);
294
+ const overflow = others.length - visible.length;
295
+
296
+ return (
297
+ <div className="dm-swarm-cockpit" data-schedule-cockpit="" data-schedule-mode="operational">
298
+ <div className="dm-swarm-section-row">
299
+ <span className="dm-run-console__hint">
300
+ {`${model.counts.total} workflow${model.counts.total === 1 ? "" : "s"} · ${model.counts.scheduled} scheduled · ${model.counts.ready} ready · ${model.counts.blocked + model.counts.drifted} blocked`}
301
+ </span>
302
+ {model.governance.blockedAttempts > 0 && (
303
+ <span className="dm-run-console__hint" title="Blocked governance attempts in the outcome stream">
304
+ {`${model.governance.blockedAttempts} blocked attempt${model.governance.blockedAttempts === 1 ? "" : "s"}`}
305
+ </span>
306
+ )}
307
+ </div>
308
+
309
+ <div className="dm-schedule-search">
310
+ <Search size={13} aria-hidden="true" />
311
+ <input
312
+ value={search}
313
+ placeholder="Search workflows or tags"
314
+ onChange={(e) => setSearch(e.target.value)}
315
+ aria-label="Search scheduled workflows"
316
+ />
317
+ </div>
318
+
319
+ <div className="dm-schedule-filters" role="tablist" aria-label="Schedule filters">
320
+ {model.filters.map((f) => (
321
+ <button
322
+ key={f.id}
323
+ type="button"
324
+ role="tab"
325
+ aria-selected={activeFilter === f.id}
326
+ className={`dm-schedule-filter${activeFilter === f.id ? " is-active" : ""}`}
327
+ onClick={() => setActiveFilter(f.id)}
328
+ >
329
+ {`${f.label} ${f.count}`}
330
+ </button>
331
+ ))}
332
+ </div>
333
+
334
+ {error && <div className="dm-helper-error" role="alert"><span>{error}</span></div>}
335
+ {notice && !error && <p className="dm-run-console__hint" data-schedule-notice="">{notice}</p>}
336
+
337
+ {attention && (
338
+ <>
339
+ <span className="dm-field-label">Needs your attention</span>
340
+ <ScheduleCard card={attention} onAction={runAction} busy={busyCard === attention.cardId} onOpen={onOpen} onSeed={onSeedSwarm} />
341
+ </>
342
+ )}
343
+
344
+ {others.length > 0 ? (
345
+ <>
346
+ <span className="dm-run-console__hint">Workflows</span>
347
+ <div className="dm-ceo-report-list" data-schedule-list="">
348
+ {visible.map((card) => (
349
+ <ScheduleCard key={card.cardId} card={card} onAction={runAction} busy={busyCard === card.cardId} onOpen={onOpen} onSeed={onSeedSwarm} />
350
+ ))}
351
+ </div>
352
+ {overflow > 0 && (
353
+ <span className="dm-run-console__hint">{`Showing ${visible.length} of ${others.length} — refine with search or filters.`}</span>
354
+ )}
355
+ </>
356
+ ) : (
357
+ !attention && <p className="dm-run-console__hint">No workflows match this filter.</p>
358
+ )}
359
+ </div>
360
+ );
361
+ }
362
+
363
+ export default ScheduleCockpit;
@@ -57,6 +57,14 @@ export const HELPER_COMMANDS = [
57
57
  mutates: false,
58
58
  view: "ceo"
59
59
  },
60
+ {
61
+ name: "/schedule",
62
+ label: "Schedule",
63
+ description: "Manage serverless scheduled workflows and upgrade eligible local workflows",
64
+ scope: "workspace",
65
+ mutates: false,
66
+ view: "schedule"
67
+ },
60
68
  {
61
69
  name: "/register-api",
62
70
  label: "Register API",