@growthub/cli 0.14.10 → 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.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/callback/route.js +35 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/failure/route.js +35 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/schedule/route.js +423 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/connect/route.js +78 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/credentials/route.js +276 -0
- 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
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/products/sync/route.js +347 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/sync/route.js +293 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/provider/connect/route.js +7 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/provider/sync/route.js +7 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/sync/route.js +197 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/apps/route.js +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/metadata-graph/route.js +1 -49
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +3 -20
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/test-api-record/route.js +3 -20
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflow/publish/route.js +407 -290
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflows/[providerId]/route.js +209 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceAddOnsMarketplace.jsx +806 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryActionCard.jsx +141 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/CeoCockpit.jsx +15 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +42 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +5 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +86 -20
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ScheduleCockpit.jsx +363 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +8 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +322 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/add-ons/add-ons-client.jsx +197 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/add-ons/page.jsx +23 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/settings-shell.jsx +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +734 -61
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +15 -10
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/env-status.js +2 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +2 -19
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +8 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/schedule-cockpit-console.js +287 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/scheduler-orchestration.js +449 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-secrets.js +77 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/serverless-readiness.js +583 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-on-callback.js +63 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-on-scheduler.js +519 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-ons.js +957 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +607 -63
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +21 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-operator-auth.js +32 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/provider.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/qstash.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/redis.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/search.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/vector.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/scripts/scheduler-ingress-smoke.mjs +26 -0
- 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 {
|
|
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
|
-
//
|
|
1540
|
-
// onboarding
|
|
1541
|
-
//
|
|
1542
|
-
const lensUnlocked =
|
|
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
|
|
1968
|
-
|
|
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
|
package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/env-status.js
CHANGED
|
@@ -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
|
-
|
|
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 {};
|
|
@@ -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:
|
|
108
|
-
?
|
|
109
|
-
|
|
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;
|