@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
@@ -70,7 +70,7 @@ import {
70
70
  nextNavItemId,
71
71
  } from "@/lib/workspace-helper-apply";
72
72
  import { listAvailableWorkflows } from "@/lib/nav-workflows";
73
- import { deriveWorkspaceActivationState, deriveLensWalkthroughState, LENS_WALKTHROUGH_DISMISS_FLAG } from "@/lib/workspace-activation";
73
+ import { deriveLensWalkthroughState, LENS_WALKTHROUGH_DISMISS_FLAG } from "@/lib/workspace-activation";
74
74
  import { WorkspaceLensWalkthrough } from "./components/WorkspaceLensWalkthrough.jsx";
75
75
  import { isHelperConfigured, WorkspaceHelperSetupModal } from "./components/WorkspaceHelperSetupModal.jsx";
76
76
 
@@ -1536,13 +1536,10 @@ export function WorkspaceRail({
1536
1536
  const workspaceName = branding.name || workspaceConfig?.name || "Growthub Workspace";
1537
1537
  const pathname = usePathname() || "/";
1538
1538
  const router = useRouter();
1539
- // Workspace Lens unlocks only after the primary activation loop completes —
1540
- // onboarding first, operating surface second. Derived from the same config
1541
- // the rail already holds, so the gate is consistent across every page.
1542
- const lensUnlocked = useMemo(
1543
- () => Boolean(deriveWorkspaceActivationState({ workspaceConfig: workspaceConfig || {} }).complete),
1544
- [workspaceConfig],
1545
- );
1539
+ // Private Agency Portal contract: Workspace Lens is an operating surface,
1540
+ // not a post-onboarding reward. Keep it visible so agents and users can
1541
+ // inspect blockers even when activation derivation is incomplete.
1542
+ const lensUnlocked = true;
1546
1543
  // One-time Workspace Lens reveal: shown anchored to the (newly visible) lens
1547
1544
  // nav item only in the in-between state, and never on the lens page itself.
1548
1545
  const lensWalkthrough = useMemo(
@@ -1579,9 +1576,14 @@ export function WorkspaceRail({
1579
1576
  const [chatSearch, setChatSearch] = useState("");
1580
1577
  const [chatExpanded, setChatExpanded] = useState(false);
1581
1578
  const [helperSetupOpen, setHelperSetupOpen] = useState(false);
1579
+ const [relativeTimesReady, setRelativeTimesReady] = useState(false);
1582
1580
  const menuWrapRef = useRef(null);
1583
1581
  const CHAT_PREVIEW_COUNT = 10;
1584
1582
 
1583
+ useEffect(() => {
1584
+ setRelativeTimesReady(true);
1585
+ }, []);
1586
+
1585
1587
  useEffect(() => {
1586
1588
  if (!openMenuId) return undefined;
1587
1589
  const onPointerDown = (e) => {
@@ -1964,8 +1966,11 @@ export function WorkspaceRail({
1964
1966
  ) : (
1965
1967
  <span className="workspace-rail-thread-title">{title}</span>
1966
1968
  )}
1967
- <span className="workspace-rail-thread-time" aria-label={`Updated ${relativeTime(row.updatedAt)}`}>
1968
- {relativeTime(row.updatedAt)}
1969
+ <span
1970
+ className="workspace-rail-thread-time"
1971
+ aria-label={relativeTimesReady ? `Updated ${relativeTime(row.updatedAt)}` : "Updated"}
1972
+ >
1973
+ {relativeTimesReady ? relativeTime(row.updatedAt) : ""}
1969
1974
  </span>
1970
1975
  </button>
1971
1976
  <div
@@ -14,18 +14,13 @@
14
14
  import { describePostgresAdapter } from "./adapters/persistence/postgres.js";
15
15
  import { describeQstashKvAdapter } from "./adapters/persistence/qstash-kv.js";
16
16
  import { describeProviderManagedAdapter } from "./adapters/persistence/provider-managed.js";
17
+ // Canonical UPPER_SNAKE candidate expansion — single source in server-secrets.js.
18
+ import { envKeyCandidates } from "./server-secrets.js";
17
19
 
18
20
  function clean(value) {
19
21
  return String(value == null ? "" : value).trim();
20
22
  }
21
23
 
22
- /** Canonical UPPER_SNAKE candidate expansion for a logical ref. */
23
- function envKeyCandidates(ref) {
24
- const token = clean(ref).replace(/[^a-z0-9]+/gi, "_").replace(/^_+|_+$/g, "").toUpperCase();
25
- if (!token) return [];
26
- return Array.from(new Set([token, `${token}_API_KEY`, `${token}_TOKEN`]));
27
- }
28
-
29
24
  /**
30
25
  * Collect every auth/env ref slug referenced by the governed config:
31
26
  * - api-registry rows: authRef
@@ -16,6 +16,7 @@ import {
16
16
  } from "./orchestration-graph.js";
17
17
  import { buildInputPayloadForRunner } from "./orchestration-run-inputs.js";
18
18
  import { runAgentSwarmGraphIfPresent } from "./orchestration-agent-swarm.js";
19
+ import { readServerSecret } from "./server-secrets.js";
19
20
 
20
21
  function normalizeMethod(value) {
21
22
  const method = String(value || "GET").trim().toUpperCase();
@@ -33,25 +34,7 @@ function buildUrl(record, inputPayload) {
33
34
  return `${baseUrl.replace(/\/+$/, "")}/${endpoint.replace(/^\/+/, "")}`;
34
35
  }
35
36
 
36
- function envKeyCandidates(ref) {
37
- const token = String(ref || "")
38
- .trim()
39
- .replace(/[^a-z0-9]+/gi, "_")
40
- .replace(/^_+|_+$/g, "")
41
- .toUpperCase();
42
- return Array.from(new Set([
43
- token,
44
- token ? `${token}_API_KEY` : "",
45
- token ? `${token}_TOKEN` : ""
46
- ].filter(Boolean)));
47
- }
48
-
49
- function readServerSecret(authRef) {
50
- for (const key of envKeyCandidates(authRef)) {
51
- if (process.env[key]) return { key, value: process.env[key] };
52
- }
53
- return null;
54
- }
37
+ // readServerSecret is the single canonical resolver from ./server-secrets.js.
55
38
 
56
39
  function buildAuthHeaders(record, secretValue) {
57
40
  if (!secretValue) return {};
@@ -62,6 +45,27 @@ function buildAuthHeaders(record, secretValue) {
62
45
  return { [headerName]: prefix ? `${prefix} ${secretValue}` : secretValue };
63
46
  }
64
47
 
48
+ function executeFeatureSeedMock(url, { registryId, method, startedAt }) {
49
+ if (!String(url || "").startsWith("mock://growthub-feature-seed/")) return null;
50
+ return {
51
+ ok: true,
52
+ exitCode: 0,
53
+ durationMs: Date.now() - startedAt,
54
+ stdout: JSON.stringify({ ok: true, status: 200, data: [{ id: "rec-1", label: "Probe record", registryId }] }, null, 2),
55
+ stderr: "",
56
+ rawPayload: { ok: true, status: 200, data: [{ id: "rec-1", label: "Probe record", registryId }] },
57
+ httpStatus: 200,
58
+ adapterMeta: {
59
+ mode: "orchestration-graph",
60
+ registryId,
61
+ url,
62
+ httpStatus: 200,
63
+ method,
64
+ transport: "feature-seed-mock"
65
+ }
66
+ };
67
+ }
68
+
65
69
  function findRegistryRecord(workspaceConfig, registryId) {
66
70
  const id = String(registryId || "").trim();
67
71
  if (!id) return null;
@@ -203,6 +207,12 @@ async function executeApiRegistryCall(workspaceConfig, nodeConfig, inputPayload,
203
207
  }
204
208
  }
205
209
 
210
+ const mockResult = executeFeatureSeedMock(url, { registryId, method, startedAt });
211
+ if (mockResult) {
212
+ clearTimeout(timer);
213
+ return mockResult;
214
+ }
215
+
206
216
  try {
207
217
  const response = await fetch(url, {
208
218
  method,
@@ -103,10 +103,14 @@ function deriveSandboxServerlessState(input = {}) {
103
103
  steps.push({
104
104
  id: "adapter",
105
105
  label: "Pick an execution adapter",
106
- status: adapterChosen ? "complete" : "active",
107
- description: adapterChosen
108
- ? `Adapter "${adapterId}".`
109
- : "Select the execution adapter for this workflow.",
106
+ status: isServerless ? (schedulerLinked ? "complete" : "active") : (adapterChosen ? "complete" : "active"),
107
+ description: isServerless
108
+ ? (schedulerLinked
109
+ ? `Execution delegates through scheduler "${schedulerId}".`
110
+ : "Link a scheduler before serverless execution can run.")
111
+ : (adapterChosen
112
+ ? `Adapter "${adapterId}".`
113
+ : "Select the execution adapter for this workflow."),
110
114
  action: adapterChosen ? null : inline({ id: "edit-adapter", label: "Choose adapter" }),
111
115
  });
112
116
 
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Schedule cockpit projection — the governed "/schedule" oversight lens over the
3
+ * existing workflow fleet (GOVERNED_COCKPIT_ENTRY_POINT_PATTERN_V1, the same
4
+ * primitive class as the CEO cockpit).
5
+ *
6
+ * PURE deriver — no React, no fetch, no fs, no config writes, no CSS. It takes
7
+ * the workspace config (+ the already-resolved `configuredEnvRefs` env-status
8
+ * signal, never secret values) and emits a low-entropy view-model the
9
+ * ScheduleCockpit component renders. It introduces NO new governed object, NO
10
+ * new API, NO new PATCH field, and NO second compatibility check: scheduler
11
+ * capability comes from the existing API Registry / marketplace rows, and
12
+ * readiness is the existing `scanServerlessReadiness` causation driver.
13
+ *
14
+ * workspace schedule cockpit =
15
+ * pure inventory + causation-derived readiness + governed action buttons
16
+ * over the existing schedule routes
17
+ *
18
+ * Every "action" on a card is a hand-off to an EXISTING governed schedule route
19
+ * (install/pause/resume/readiness/uninstall) or the Add-ons marketplace setup
20
+ * path — the cockpit never schedules, never mutates config, never PATCHes a row.
21
+ */
22
+
23
+ import { parseOrchestrationGraph } from "./orchestration-graph.js";
24
+ import { scanServerlessReadiness, READINESS_DELTA_TAGS } from "./serverless-readiness.js";
25
+
26
+ function clean(value) {
27
+ return String(value == null ? "" : value).trim();
28
+ }
29
+
30
+ function truthy(value) {
31
+ return ["true", "1", "on", "yes"].includes(clean(value).toLowerCase()) || value === true;
32
+ }
33
+
34
+ function isApiRegistryObject(object) {
35
+ const objectType = clean(object?.objectType);
36
+ const id = clean(object?.id || object?.objectId);
37
+ return objectType === "api-registry" || id === "api-registry";
38
+ }
39
+
40
+ const SERVERLESS_SCHEDULER_LANE = "serverless-scheduler";
41
+ // First-class default provider integration id — see workspace-add-ons.js. Kept
42
+ // as a known slug ONLY to label/route the Upstash setup shortcut; everything
43
+ // else is provider-agnostic via executionLane + schedulerRegistryId.
44
+ const UPSTASH_QSTASH_INTEGRATION_ID = "upstash-qstash-workflow";
45
+
46
+ /** Detect every scheduler-capable product/provider from existing governed state. */
47
+ function detectSchedulerProducts(workspaceConfig) {
48
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
49
+ const products = [];
50
+ for (const object of objects) {
51
+ if (!isApiRegistryObject(object)) continue;
52
+ for (const row of Array.isArray(object.rows) ? object.rows : []) {
53
+ if (clean(row?.executionLane) !== SERVERLESS_SCHEDULER_LANE) continue;
54
+ const integrationId = clean(row?.integrationId);
55
+ if (!integrationId) continue;
56
+ const verified = clean(row?.syncStatus) === "verified";
57
+ const isUpstash = integrationId === UPSTASH_QSTASH_INTEGRATION_ID || clean(row?.productId) === "upstash-qstash";
58
+ products.push({
59
+ integrationId,
60
+ label: clean(row?.Name) || integrationId,
61
+ productId: clean(row?.productId),
62
+ providerId: clean(row?.providerId) || (isUpstash ? "upstash" : ""),
63
+ verified,
64
+ provider: isUpstash ? "QStash" : "Custom",
65
+ custom: !isUpstash,
66
+ region: clean(row?.region),
67
+ });
68
+ }
69
+ }
70
+ return products;
71
+ }
72
+
73
+ /** Resolve the api-registry-call node's registry id (the data dependency). */
74
+ function resolveDependencyRegistryId(row) {
75
+ const graph = parseOrchestrationGraph(row?.orchestrationGraph || row?.orchestrationConfig);
76
+ const apiNode = (graph?.nodes || []).find((n) => n?.type === "api-registry-call");
77
+ return clean(apiNode?.config?.registryId || apiNode?.config?.integrationId);
78
+ }
79
+
80
+ // Friendly chip labels for the canonical readiness delta tags (no new vocab).
81
+ const DELTA_TAG_CHIP = {
82
+ [READINESS_DELTA_TAGS.RUNTIME_LOCALITY]: "Runtime locality",
83
+ [READINESS_DELTA_TAGS.LOCAL_AGENT_UPGRADE_REQUIRED]: "Local agent upgrade required",
84
+ [READINESS_DELTA_TAGS.MISSING_SERVER_SECRET]: "Missing secret",
85
+ [READINESS_DELTA_TAGS.API_REGISTRY_ENV]: "API Registry env",
86
+ [READINESS_DELTA_TAGS.INPUT_CONTRACT]: "Input contract",
87
+ [READINESS_DELTA_TAGS.SCHEDULED_INPUT_UNMAPPED]: "Scheduled input unmapped",
88
+ [READINESS_DELTA_TAGS.DOWNSTREAM_NODE_INCOMPATIBLE]: "Downstream incompatible",
89
+ [READINESS_DELTA_TAGS.PUBLISHED_GRAPH_REQUIRED]: "Published graph required",
90
+ [READINESS_DELTA_TAGS.SERVERLESS_SCHEDULE]: "Serverless schedule",
91
+ };
92
+
93
+ // One report state per workflow row — drift takes priority over scheduled so a
94
+ // continuing contract that no longer proves out is never shown as healthy.
95
+ function classifyState({ serverless, hasSchedule, paused, readinessOk }) {
96
+ if (serverless && hasSchedule) {
97
+ if (paused) return "paused";
98
+ if (!readinessOk) return "drifted";
99
+ return "scheduled";
100
+ }
101
+ if (!readinessOk) return "blocked";
102
+ return "ready"; // local + clean → ready to schedule (provider-gated in nextAction)
103
+ }
104
+
105
+ const STATE_FILTER = {
106
+ scheduled: "scheduled",
107
+ paused: "paused",
108
+ ready: "ready",
109
+ blocked: "blocked",
110
+ drifted: "blocked",
111
+ };
112
+
113
+ function nextActionForCard({ state, hasProvider, custom }) {
114
+ switch (state) {
115
+ case "scheduled":
116
+ return { kind: "manage", label: "Manage schedule" };
117
+ case "paused":
118
+ return { kind: "resume", label: "Resume schedule" };
119
+ case "drifted":
120
+ return { kind: "readiness", label: "Run readiness scan" };
121
+ case "blocked":
122
+ return { kind: "readiness", label: "Resolve & rescan" };
123
+ case "ready":
124
+ default:
125
+ return hasProvider
126
+ ? { kind: "schedule", label: "Upgrade to Serverless Schedule" }
127
+ : { kind: "setup-provider", label: "Set up scheduler" };
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Build the schedule cockpit view-model.
133
+ *
134
+ * @param {object} args
135
+ * @param {object} args.workspaceConfig
136
+ * @param {string[]} [args.configuredEnvRefs] resolved credential ref slugs (env-status)
137
+ * @param {Array} [args.receipts] workspace outcome receipts (optional rollup)
138
+ * @returns {object} view-model consumed by ScheduleCockpit.jsx
139
+ */
140
+ export function deriveScheduleCockpit({ workspaceConfig, configuredEnvRefs = [], receipts = [] } = {}) {
141
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
142
+ const installedSchedulerProducts = detectSchedulerProducts(workspaceConfig);
143
+ const hasProvider = installedSchedulerProducts.length > 0;
144
+ const schedulerSetupState = hasProvider ? "installed" : "none";
145
+
146
+ const cards = [];
147
+ objects.forEach((object) => {
148
+ if (clean(object?.objectType) !== "sandbox-environment") return;
149
+ const objectId = clean(object?.id);
150
+ const objectLabel = clean(object?.label || object?.name) || objectId;
151
+ (Array.isArray(object.rows) ? object.rows : []).forEach((row, index) => {
152
+ const name = clean(row?.Name);
153
+ if (!name) return;
154
+ const serverless = clean(row?.runLocality).toLowerCase() === "serverless";
155
+ const scheduleId = clean(row?.scheduleId);
156
+ const hasSchedule = Boolean(scheduleId);
157
+ const paused = truthy(row?.schedulerPaused);
158
+ const phase = serverless && hasSchedule ? "bound" : "pre-bind";
159
+ const readiness = scanServerlessReadiness({
160
+ row,
161
+ workspaceConfig,
162
+ configuredEnvRefs,
163
+ phase,
164
+ expected: {
165
+ scheduleId,
166
+ schedulerRegistryId: clean(row?.schedulerRegistryId),
167
+ providerId: clean(row?.schedulerProviderId),
168
+ productId: clean(row?.schedulerProductId),
169
+ },
170
+ });
171
+ const state = classifyState({ serverless, hasSchedule, paused, readinessOk: readiness.ok });
172
+ const dependencyRegistryId = resolveDependencyRegistryId(row);
173
+ const providerId = clean(row?.schedulerProviderId);
174
+ const productId = clean(row?.schedulerProductId);
175
+ const product = installedSchedulerProducts.find(
176
+ (p) => p.integrationId === clean(row?.schedulerRegistryId),
177
+ ) || null;
178
+ const custom = product ? product.custom : false;
179
+ const lastRunStatus = clean(row?.lastScheduledRunStatus);
180
+ const lastRunFailed = clean(row?.lastScheduledRunFailureReason) !== "" || (lastRunStatus && !lastRunStatus.startsWith("2"));
181
+
182
+ // Compact, scannable tags — state + readiness deltas + provider + run signal.
183
+ const tags = [];
184
+ if (state === "scheduled") tags.push("Scheduled");
185
+ if (state === "paused") tags.push("Paused");
186
+ if (state === "ready") tags.push("Ready to schedule");
187
+ if (state === "blocked") tags.push("Blocked");
188
+ if (state === "drifted") tags.push("Serverless drift");
189
+ if (!serverless && state !== "blocked") tags.push("Local-only");
190
+ for (const tag of readiness.deltaTags || []) {
191
+ const chip = DELTA_TAG_CHIP[tag];
192
+ if (chip && !tags.includes(chip)) tags.push(chip);
193
+ }
194
+ if (product) tags.push(product.provider);
195
+ else if (serverless && hasSchedule) tags.push("Custom scheduler");
196
+ if (serverless && hasSchedule && lastRunFailed) tags.push("Last run failed");
197
+ if (serverless && hasSchedule && !lastRunStatus) tags.push("No receipt yet");
198
+
199
+ cards.push({
200
+ cardId: `${objectId}::${clean(row?.id) || name}::${index}`,
201
+ objectId,
202
+ objectLabel,
203
+ name,
204
+ state,
205
+ filterBucket: STATE_FILTER[state] || state,
206
+ locality: serverless ? "serverless" : "local",
207
+ provider: product?.provider || (serverless && hasSchedule ? "Custom" : ""),
208
+ providerId,
209
+ productId,
210
+ schedulerRegistryId: clean(row?.schedulerRegistryId),
211
+ dependencyRegistryId,
212
+ scheduleId,
213
+ cron: clean(row?.schedulerCron),
214
+ region: clean(row?.schedulerRegion) || product?.region || "",
215
+ paused,
216
+ lastSync: clean(row?.lastScheduledRunAt) || clean(row?.schedulerInstalledAt),
217
+ lastRunStatus,
218
+ lastRunFailed: Boolean(serverless && hasSchedule && lastRunFailed),
219
+ readiness: {
220
+ ok: readiness.ok,
221
+ status: readiness.status,
222
+ deltaTags: readiness.deltaTags || [],
223
+ blockingNodes: readiness.blockingNodes || [],
224
+ warnings: readiness.warnings || [],
225
+ helperActions: (readiness.blockingNodes || []).map((n) => n.helperAction).filter(Boolean),
226
+ },
227
+ tags,
228
+ custom,
229
+ nextAction: nextActionForCard({ state, hasProvider, custom }),
230
+ // The governed artifact the "Open" affordance hands off to (the canvas).
231
+ artifact: { surface: "workflow-canvas", objectId, name },
232
+ });
233
+ });
234
+ });
235
+
236
+ const countOf = (s) => cards.filter((c) => c.state === s).length;
237
+ const counts = {
238
+ total: cards.length,
239
+ scheduled: countOf("scheduled"),
240
+ paused: countOf("paused"),
241
+ ready: countOf("ready"),
242
+ blocked: countOf("blocked"),
243
+ drifted: countOf("drifted"),
244
+ localOnly: cards.filter((c) => c.locality === "local").length,
245
+ missingSecret: cards.filter((c) => (c.readiness.deltaTags || []).includes(READINESS_DELTA_TAGS.MISSING_SERVER_SECRET)).length,
246
+ };
247
+
248
+ // Filters the sidecar exposes — only those with members (All always present).
249
+ const filters = [
250
+ { id: "all", label: "All", count: cards.length },
251
+ { id: "scheduled", label: "Scheduled", count: counts.scheduled + counts.paused },
252
+ { id: "ready", label: "Ready", count: counts.ready },
253
+ { id: "blocked", label: "Blocked", count: counts.blocked + counts.drifted },
254
+ { id: "local", label: "Local-only", count: counts.localOnly },
255
+ { id: "missing-secret", label: "Missing secrets", count: counts.missingSecret },
256
+ { id: "qstash", label: "Provider: QStash", count: cards.filter((c) => c.provider === "QStash").length },
257
+ { id: "custom", label: "Provider: Custom", count: cards.filter((c) => c.provider === "Custom").length },
258
+ ].filter((f) => f.id === "all" || f.count > 0);
259
+
260
+ // The single highest-value next move (drift → blocked → ready), or null.
261
+ const ATTENTION = ["drifted", "blocked", "ready"];
262
+ let attention = null;
263
+ for (const s of ATTENTION) {
264
+ const hit = cards.find((c) => c.state === s);
265
+ if (hit) { attention = hit; break; }
266
+ }
267
+
268
+ const safeReceipts = Array.isArray(receipts) ? receipts : [];
269
+ const blockedAttempts = safeReceipts.filter((r) => r && r.outcomeStatus === "blocked").length;
270
+
271
+ return {
272
+ title: "Schedule Cockpit",
273
+ schedulerSetupState,
274
+ installedSchedulerProducts,
275
+ defaultProvider: installedSchedulerProducts[0] || null,
276
+ workflowCards: cards,
277
+ filters,
278
+ defaultFilter: "all",
279
+ attention,
280
+ counts,
281
+ governance: { blockedAttempts },
282
+ setupRoute: "/settings/add-ons",
283
+ generatedFromReceipts: safeReceipts.length > 0,
284
+ };
285
+ }
286
+
287
+ export default deriveScheduleCockpit;