@burdenoff/microfe-vibecontrols 2026.528.7 → 2026.528.9

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 (37) hide show
  1. package/dist/VibeControlsRoot.js +1 -0
  2. package/dist/VibeControlsRoot.js.map +1 -1
  3. package/dist/components/agent-manager/AgentManagerInputArea.js +70 -64
  4. package/dist/components/agent-manager/AgentManagerInputArea.js.map +1 -1
  5. package/dist/components/audit/AuditLogFilters.js +71 -47
  6. package/dist/components/audit/AuditLogFilters.js.map +1 -1
  7. package/dist/components/shared/AgentSetupBanner.js +50 -45
  8. package/dist/components/shared/AgentSetupBanner.js.map +1 -1
  9. package/dist/components/shared/LinkToVibePanel.js +97 -0
  10. package/dist/components/shared/LinkToVibePanel.js.map +1 -0
  11. package/dist/components/shared/index.js +1 -0
  12. package/dist/components/vibe/VibeLinkedEntitiesPanel.js +60 -0
  13. package/dist/components/vibe/VibeLinkedEntitiesPanel.js.map +1 -0
  14. package/dist/components/vibedeck/VibeDeckButtonTile.js +25 -23
  15. package/dist/components/vibedeck/VibeDeckButtonTile.js.map +1 -1
  16. package/dist/components/vibedeck/VibeDeckGrid.js +2 -2
  17. package/dist/components/vibedeck/VibeDeckGrid.js.map +1 -1
  18. package/dist/microfe-vibecontrols.css +1 -1
  19. package/dist/pages/AgentDetailsPage.js +2 -2
  20. package/dist/pages/AgentDetailsPage.js.map +1 -1
  21. package/dist/pages/AnalyticsPage.js +255 -204
  22. package/dist/pages/AnalyticsPage.js.map +1 -1
  23. package/dist/pages/OverviewPage.js +1 -11
  24. package/dist/pages/OverviewPage.js.map +1 -1
  25. package/dist/pages/SessionsPage.js +825 -629
  26. package/dist/pages/SessionsPage.js.map +1 -1
  27. package/dist/pages/VibeAuditPage.js +184 -153
  28. package/dist/pages/VibeAuditPage.js.map +1 -1
  29. package/dist/pages/VibeDeckPage.js +416 -379
  30. package/dist/pages/VibeDeckPage.js.map +1 -1
  31. package/dist/pages/VibeDetailsPage.js +621 -557
  32. package/dist/pages/VibeDetailsPage.js.map +1 -1
  33. package/dist/pages/tunnels/TunnelsTab.js +115 -90
  34. package/dist/pages/tunnels/TunnelsTab.js.map +1 -1
  35. package/dist/services/schedulerApi.js +37 -15
  36. package/dist/services/schedulerApi.js.map +1 -1
  37. package/package.json +1 -1
@@ -2,51 +2,64 @@ import { useTr as e } from "../../shared/hooks/useTr.js";
2
2
  import { ConfirmDialog as t } from "../../components/shared/ConfirmDialog.js";
3
3
  import { QuotaExhaustedDialog as n } from "../../components/shared/QuotaExhaustedDialog.js";
4
4
  import { getQuotaDialogContent as r, isQuotaExhaustedError as i, markQuotaHandledLocally as a } from "../../utils/quotaUtils.js";
5
- import { useDeleteTunnelMutation as o, useListTunnelsQuery as s, useStartTunnelMutation as c, useStopTunnelMutation as l } from "../../generated/wspace-operations.js";
6
- import { PageHeader as u } from "../../components/shared/PageHeader.js";
7
- import { LoadingState as d } from "../../components/shared/LoadingState.js";
8
- import { ErrorState as f } from "../../components/shared/ErrorState.js";
9
- import { EmptyState as p } from "../../components/shared/EmptyState.js";
10
- import { SearchInput as m } from "../../components/shared/SearchInput.js";
11
- import { FilterDropdown as h } from "../../components/shared/FilterDropdown.js";
12
- import { TunnelCard as g } from "../../components/tunnels/TunnelCard.js";
13
- import { useEffect as _, useMemo as v, useState as y } from "react";
14
- import { Link as b, useNavigate as x } from "react-router-dom";
15
- import { motion as S } from "framer-motion";
16
- import { jsx as C, jsxs as w } from "react/jsx-runtime";
5
+ import { useDeleteTunnelMutation as o, useListAgentsQuery as s, useListTunnelsQuery as c, useStartTunnelMutation as l, useStopTunnelMutation as u } from "../../generated/wspace-operations.js";
6
+ import { PageHeader as d } from "../../components/shared/PageHeader.js";
7
+ import { LoadingState as f } from "../../components/shared/LoadingState.js";
8
+ import { ErrorState as p } from "../../components/shared/ErrorState.js";
9
+ import { EmptyState as m } from "../../components/shared/EmptyState.js";
10
+ import { SearchInput as h } from "../../components/shared/SearchInput.js";
11
+ import { FilterDropdown as g } from "../../components/shared/FilterDropdown.js";
12
+ import { TunnelCard as _ } from "../../components/tunnels/TunnelCard.js";
13
+ import { useEffect as v, useMemo as y, useState as b } from "react";
14
+ import { Link as x, useNavigate as S } from "react-router-dom";
15
+ import { motion as C } from "framer-motion";
16
+ import { jsx as w, jsxs as T } from "react/jsx-runtime";
17
+ import { Button as E } from "@burdenoff/fe-libs/ui";
17
18
  //#region src/pages/tunnels/TunnelsTab.tsx
18
- var T = 4e3;
19
- function E() {
20
- let E = e(), D = x(), [O, k] = y(""), [A, j] = y("all"), [M, N] = y("all"), [P, F] = y(null), [I, L] = y(!1), [R, z] = y(/* @__PURE__ */ new Set()), { data: B, loading: V, error: H, refetch: U, startPolling: W, stopPolling: G } = s({
19
+ var D = 4e3;
20
+ function O() {
21
+ let O = e(), k = S(), [A, ee] = b(""), [j, M] = b("all"), [N, P] = b("all"), [F, I] = b(null), [L, R] = b(!1), [z, B] = b(/* @__PURE__ */ new Set()), { data: V, loading: H, error: U, refetch: W, startPolling: G, stopPolling: K } = c({
21
22
  variables: {
22
- filter: v(() => {
23
+ filter: y(() => {
23
24
  let e = {};
24
- return A !== "all" && (e.providerKey = A), M !== "all" && (e.status = [M]), O.trim() && (e.search = O.trim()), e;
25
+ return j !== "all" && (e.providerKey = j), N !== "all" && (e.status = [N]), A.trim() && (e.search = A.trim()), e;
25
26
  }, [
26
- A,
27
- M,
28
- O
27
+ j,
28
+ N,
29
+ A
29
30
  ]),
30
31
  pagination: { limit: 50 }
31
32
  },
32
33
  fetchPolicy: "cache-and-network"
33
- }), K = B?.tunnels?.items ?? [], q = R.size > 0 || K.some((e) => e.status === "PROVISIONING" || e.status === "DELETING" || e.status === "ACTIVE" && !e.primaryDomain?.hostname);
34
- _(() => {
35
- q ? W(T) : G();
34
+ }), { data: q } = s({
35
+ variables: { pagination: { limit: 1 } },
36
+ fetchPolicy: "cache-first"
37
+ }), J = (q?.vibecontrolsAgents?.length ?? 0) > 0, Y = V?.tunnels?.items ?? [], X = (e) => {
38
+ if (!J) {
39
+ e.preventDefault();
40
+ let t = {
41
+ message: O("vibecontrols.tunnels.list.createAgentFirst", "Create an agent first to use tunnels"),
42
+ variant: "info"
43
+ };
44
+ window.dispatchEvent(new CustomEvent("vibecontrols:toast", { detail: t })), k("/vibecontrols/agents");
45
+ }
46
+ }, Z = z.size > 0 || Y.some((e) => e.status === "PROVISIONING" || e.status === "DELETING" || e.status === "ACTIVE" && !e.primaryDomain?.hostname);
47
+ v(() => {
48
+ Z ? G(D) : K();
36
49
  }, [
37
- q,
38
- W,
39
- G
50
+ Z,
51
+ G,
52
+ K
40
53
  ]);
41
- let J = (e) => z((t) => new Set([...t, e])), Y = (e) => z((t) => {
54
+ let Q = (e) => B((t) => new Set([...t, e])), $ = (e) => B((t) => {
42
55
  let n = new Set(t);
43
56
  return n.delete(e), n;
44
- }), [X] = c(), Z = async (e) => {
45
- let t = K.find((t) => t.id === e);
57
+ }), [te] = l(), ne = async (e) => {
58
+ let t = Y.find((t) => t.id === e);
46
59
  if (t) {
47
- J(e);
60
+ Q(e);
48
61
  try {
49
- let n = await X({
62
+ let n = await te({
50
63
  variables: { id: e },
51
64
  optimisticResponse: { startTunnel: {
52
65
  ...t,
@@ -54,19 +67,19 @@ function E() {
54
67
  __typename: "Tunnel"
55
68
  } }
56
69
  });
57
- n.error && i(n.error) && (a(), L(!0));
70
+ n.error && i(n.error) && (a(), R(!0));
58
71
  } catch (e) {
59
- i(e) ? (a(), L(!0)) : console.error("[TunnelsTab] startTunnel failed", e);
72
+ i(e) ? (a(), R(!0)) : console.error("[TunnelsTab] startTunnel failed", e);
60
73
  } finally {
61
- Y(e);
74
+ $(e);
62
75
  }
63
76
  }
64
- }, [Q] = l(), $ = async (e) => {
65
- let t = K.find((t) => t.id === e);
77
+ }, [re] = u(), ie = async (e) => {
78
+ let t = Y.find((t) => t.id === e);
66
79
  if (t) {
67
- J(e);
80
+ Q(e);
68
81
  try {
69
- await Q({
82
+ await re({
70
83
  variables: { id: e },
71
84
  optimisticResponse: { stopTunnel: {
72
85
  ...t,
@@ -75,39 +88,43 @@ function E() {
75
88
  } }
76
89
  });
77
90
  } finally {
78
- Y(e);
91
+ $(e);
79
92
  }
80
93
  }
81
- }, [ee, { loading: te }] = o({ onCompleted: () => {
82
- F(null), U();
94
+ }, [ae, { loading: oe }] = o({ onCompleted: () => {
95
+ I(null), W();
83
96
  } });
84
- return /* @__PURE__ */ w("div", {
97
+ return /* @__PURE__ */ T("div", {
85
98
  className: "p-6 max-w-7xl mx-auto",
86
99
  children: [
87
- /* @__PURE__ */ C(u, {
88
- title: E("vibecontrols.tunnels.list.title", "Tunnels"),
89
- description: E("vibecontrols.tunnels.list.description", "Expose local services on *.vibetunnels.com and custom domains."),
90
- actions: /* @__PURE__ */ C(b, {
91
- to: "/vibecontrols/tunnels/new",
92
- className: "inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-fg hover:bg-action-primary-bg-hover transition-colors",
93
- children: E("vibecontrols.tunnels.list.newTunnel", "New tunnel")
100
+ /* @__PURE__ */ w(d, {
101
+ title: O("vibecontrols.tunnels.list.title", "Tunnels"),
102
+ description: O("vibecontrols.tunnels.list.description", "Expose local services on *.vibetunnels.com and custom domains."),
103
+ actions: /* @__PURE__ */ w(E, {
104
+ variant: "default",
105
+ asChild: !0,
106
+ children: /* @__PURE__ */ w(x, {
107
+ to: "/vibecontrols/tunnels/new",
108
+ onClick: X,
109
+ children: O("vibecontrols.tunnels.list.newTunnel", "New tunnel")
110
+ })
94
111
  })
95
112
  }),
96
- /* @__PURE__ */ w("div", {
113
+ /* @__PURE__ */ T("div", {
97
114
  className: "flex flex-col sm:flex-row gap-3 mb-4",
98
115
  children: [
99
- /* @__PURE__ */ C("div", {
116
+ /* @__PURE__ */ w("div", {
100
117
  className: "flex-1",
101
- children: /* @__PURE__ */ C(m, {
102
- value: O,
103
- onChange: k,
118
+ children: /* @__PURE__ */ w(h, {
119
+ value: A,
120
+ onChange: ee,
104
121
  placeholder: "Search tunnels by name or slug"
105
122
  })
106
123
  }),
107
- /* @__PURE__ */ C(h, {
124
+ /* @__PURE__ */ w(g, {
108
125
  label: "Provider",
109
- value: A,
110
- onChange: j,
126
+ value: j,
127
+ onChange: M,
111
128
  options: [
112
129
  {
113
130
  value: "all",
@@ -123,10 +140,10 @@ function E() {
123
140
  }
124
141
  ]
125
142
  }),
126
- /* @__PURE__ */ C(h, {
143
+ /* @__PURE__ */ w(g, {
127
144
  label: "Status",
128
- value: M,
129
- onChange: N,
145
+ value: N,
146
+ onChange: P,
130
147
  options: [
131
148
  {
132
149
  value: "all",
@@ -156,55 +173,63 @@ function E() {
156
173
  })
157
174
  ]
158
175
  }),
159
- V && !B && /* @__PURE__ */ C(d, { message: "Loading tunnels" }),
160
- H && /* @__PURE__ */ C(f, {
176
+ H && !V && /* @__PURE__ */ w(f, { message: "Loading tunnels" }),
177
+ U && /* @__PURE__ */ w(p, {
161
178
  title: "Failed to load tunnels",
162
- message: H.message,
163
- onRetry: () => U()
179
+ message: U.message,
180
+ onRetry: () => W()
181
+ }),
182
+ V && Y.length === 0 && !J && /* @__PURE__ */ w(m, {
183
+ title: O("vibecontrols.tunnels.list.noAgentTitle", "Create an agent first"),
184
+ description: O("vibecontrols.tunnels.list.noAgentDescription", "Tunnels need a VibeControls agent to forward traffic. Install + register an agent, then come back to create your first tunnel."),
185
+ action: /* @__PURE__ */ w(E, {
186
+ variant: "default",
187
+ onClick: () => k("/vibecontrols/agents"),
188
+ children: O("vibecontrols.tunnels.list.setupAgent", "Set up an agent")
189
+ })
164
190
  }),
165
- B && K.length === 0 && /* @__PURE__ */ C(p, {
166
- title: E("vibecontrols.tunnels.list.emptyTitle", "No tunnels yet"),
167
- description: E("vibecontrols.tunnels.list.emptyDescription", "Create your first tunnel to expose a local service."),
168
- action: /* @__PURE__ */ C("button", {
169
- type: "button",
170
- onClick: () => D("/vibecontrols/tunnels/new"),
171
- className: "inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-fg hover:bg-action-primary-bg-hover transition-colors",
172
- children: E("vibecontrols.tunnels.list.createTunnel", "Create tunnel")
191
+ V && Y.length === 0 && J && /* @__PURE__ */ w(m, {
192
+ title: O("vibecontrols.tunnels.list.emptyTitle", "No tunnels yet"),
193
+ description: O("vibecontrols.tunnels.list.emptyDescription", "Create your first tunnel to expose a local service."),
194
+ action: /* @__PURE__ */ w(E, {
195
+ variant: "default",
196
+ onClick: () => k("/vibecontrols/tunnels/new"),
197
+ children: O("vibecontrols.tunnels.list.createTunnel", "Create tunnel")
173
198
  })
174
199
  }),
175
- K.length > 0 && /* @__PURE__ */ C(S.div, {
200
+ Y.length > 0 && /* @__PURE__ */ w(C.div, {
176
201
  initial: { opacity: 0 },
177
202
  animate: { opacity: 1 },
178
203
  className: "space-y-3",
179
- children: K.map((e) => /* @__PURE__ */ C(g, {
204
+ children: Y.map((e) => /* @__PURE__ */ w(_, {
180
205
  tunnel: e,
181
- isPending: R.has(e.id),
182
- onOpen: (e) => D(`/vibecontrols/tunnels/${e}`),
183
- onStart: (e) => void Z(e),
184
- onStop: (e) => void $(e),
185
- onDelete: (e, t) => F({
206
+ isPending: z.has(e.id),
207
+ onOpen: (e) => k(`/vibecontrols/tunnels/${e}`),
208
+ onStart: (e) => void ne(e),
209
+ onStop: (e) => void ie(e),
210
+ onDelete: (e, t) => I({
186
211
  id: e,
187
212
  name: t
188
213
  })
189
214
  }, e.id))
190
215
  }),
191
- /* @__PURE__ */ C(t, {
192
- isOpen: P !== null,
193
- onClose: () => F(null),
216
+ /* @__PURE__ */ w(t, {
217
+ isOpen: F !== null,
218
+ onClose: () => I(null),
194
219
  onConfirm: () => {
195
- P && ee({ variables: { id: P.id } });
220
+ F && ae({ variables: { id: F.id } });
196
221
  },
197
222
  title: "Delete tunnel",
198
- message: P ? `Delete "${P.name}"? This will stop the tunnel and remove all its sessions and domains.` : "",
223
+ message: F ? `Delete "${F.name}"? This will stop the tunnel and remove all its sessions and domains.` : "",
199
224
  confirmLabel: "Delete",
200
225
  variant: "danger",
201
- isLoading: te
226
+ isLoading: oe
202
227
  }),
203
- /* @__PURE__ */ C(n, {
204
- isOpen: I,
205
- onClose: () => L(!1),
228
+ /* @__PURE__ */ w(n, {
229
+ isOpen: L,
230
+ onClose: () => R(!1),
206
231
  onConfirm: () => {
207
- L(!1), D("/billing");
232
+ R(!1), k("/billing");
208
233
  },
209
234
  ...r("tunnel-start")
210
235
  })
@@ -212,6 +237,6 @@ function E() {
212
237
  });
213
238
  }
214
239
  //#endregion
215
- export { E as TunnelsTab };
240
+ export { O as TunnelsTab };
216
241
 
217
242
  //# sourceMappingURL=TunnelsTab.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TunnelsTab.js","names":[],"sources":["../../../src/pages/tunnels/TunnelsTab.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { Link, useNavigate } from 'react-router-dom';\nimport { motion } from 'framer-motion';\n\nimport { PageHeader } from '@/components/shared/PageHeader';\nimport { SearchInput } from '@/components/shared/SearchInput';\nimport { FilterDropdown } from '@/components/shared/FilterDropdown';\nimport { ConfirmDialog } from '@/components/shared/ConfirmDialog';\nimport { QuotaExhaustedDialog } from '@/components/shared/QuotaExhaustedDialog';\nimport { EmptyState } from '@/components/shared/EmptyState';\nimport { LoadingState } from '@/components/shared/LoadingState';\nimport { ErrorState } from '@/components/shared/ErrorState';\nimport { TunnelCard } from '@/components/tunnels/TunnelCard';\nimport type { TunnelStatus, TunnelProviderKey } from '@/generated/wspace-types';\nimport { useTr } from '../../shared/hooks/useTr';\nimport {\n useListTunnelsQuery,\n useStartTunnelMutation,\n useStopTunnelMutation,\n useDeleteTunnelMutation,\n type ListTunnelsQuery,\n} from '@/generated/wspace-operations';\nimport {\n isQuotaExhaustedError,\n markQuotaHandledLocally,\n getQuotaDialogContent,\n} from '@/utils/quotaUtils';\n\ntype TunnelRow = NonNullable<ListTunnelsQuery['tunnels']>['items'][number];\n\nconst TRANSITION_POLL_MS = 4000;\n\nexport function TunnelsTab() {\n const tr = useTr();\n const navigate = useNavigate();\n const [search, setSearch] = useState('');\n const [providerFilter, setProviderFilter] = useState<string>('all');\n const [statusFilter, setStatusFilter] = useState<string>('all');\n const [pendingDelete, setPendingDelete] = useState<{ id: string; name: string } | null>(null);\n const [showQuotaExhausted, setShowQuotaExhausted] = useState(false);\n const [pendingIds, setPendingIds] = useState<Set<string>>(new Set());\n\n const filter = useMemo(() => {\n const f: Record<string, unknown> = {};\n if (providerFilter !== 'all') {\n f.providerKey = providerFilter as TunnelProviderKey;\n }\n if (statusFilter !== 'all') {\n f.status = [statusFilter as TunnelStatus];\n }\n if (search.trim()) {\n f.search = search.trim();\n }\n return f;\n }, [providerFilter, statusFilter, search]);\n\n const { data, loading, error, refetch, startPolling, stopPolling } = useListTunnelsQuery({\n variables: { filter, pagination: { limit: 50 } },\n fetchPolicy: 'cache-and-network',\n });\n\n const tunnels: TunnelRow[] = data?.tunnels?.items ?? [];\n\n // Poll while any tunnel is in a transient state OR is ACTIVE but has no\n // domain yet (Cloudflare assigns the URL ~5-15 s after start returns).\n const needsPoll =\n pendingIds.size > 0 ||\n tunnels.some(\n (t) =>\n t.status === 'PROVISIONING' ||\n t.status === 'DELETING' ||\n (t.status === 'ACTIVE' && !t.primaryDomain?.hostname)\n );\n useEffect(() => {\n if (needsPoll) {\n startPolling(TRANSITION_POLL_MS);\n } else {\n stopPolling();\n }\n }, [needsPoll, startPolling, stopPolling]);\n\n const addPending = (id: string) => setPendingIds((prev) => new Set([...prev, id]));\n const removePending = (id: string) =>\n setPendingIds((prev) => {\n const next = new Set(prev);\n next.delete(id);\n return next;\n });\n\n const [startTunnel] = useStartTunnelMutation();\n\n const handleStartTunnel = async (id: string) => {\n const existing = tunnels.find((t) => t.id === id);\n if (!existing) return;\n addPending(id);\n try {\n const result = await startTunnel({\n variables: { id },\n optimisticResponse: {\n startTunnel: {\n ...existing,\n status: 'PROVISIONING' as TunnelStatus,\n __typename: 'Tunnel',\n },\n },\n });\n if (result.error && isQuotaExhaustedError(result.error)) {\n markQuotaHandledLocally();\n setShowQuotaExhausted(true);\n }\n } catch (err) {\n // Only open the quota modal for actual quota errors — pre-existing\n // bug spotted on the BOFF-2625 review: any tunnel-start failure\n // (network / validation / auth) was opening the quota dialog.\n if (isQuotaExhaustedError(err)) {\n markQuotaHandledLocally();\n setShowQuotaExhausted(true);\n } else {\n console.error('[TunnelsTab] startTunnel failed', err);\n }\n } finally {\n removePending(id);\n }\n };\n\n const [stopTunnel] = useStopTunnelMutation();\n\n const handleStopTunnel = async (id: string) => {\n const existing = tunnels.find((t) => t.id === id);\n if (!existing) return;\n addPending(id);\n try {\n await stopTunnel({\n variables: { id },\n optimisticResponse: {\n stopTunnel: { ...existing, status: 'STOPPED' as TunnelStatus, __typename: 'Tunnel' },\n },\n });\n } finally {\n removePending(id);\n }\n };\n\n const [deleteTunnel, { loading: deleting }] = useDeleteTunnelMutation({\n onCompleted: () => {\n setPendingDelete(null);\n void refetch();\n },\n });\n\n return (\n <div className=\"p-6 max-w-7xl mx-auto\">\n <PageHeader\n title={tr('vibecontrols.tunnels.list.title', 'Tunnels')}\n description={tr(\n 'vibecontrols.tunnels.list.description',\n 'Expose local services on *.vibetunnels.com and custom domains.'\n )}\n actions={\n <Link\n to=\"/vibecontrols/tunnels/new\"\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-fg hover:bg-action-primary-bg-hover transition-colors\"\n >\n {tr('vibecontrols.tunnels.list.newTunnel', 'New tunnel')}\n </Link>\n }\n />\n\n <div className=\"flex flex-col sm:flex-row gap-3 mb-4\">\n <div className=\"flex-1\">\n <SearchInput\n value={search}\n onChange={setSearch}\n placeholder=\"Search tunnels by name or slug\"\n />\n </div>\n <FilterDropdown\n label=\"Provider\"\n value={providerFilter}\n onChange={setProviderFilter}\n options={[\n { value: 'all', label: 'All providers' },\n { value: 'VIBETUNNELS', label: 'Vibe Tunnels (default)' },\n { value: 'CLOUDFLARE', label: 'Cloudflare Tunnels' },\n ]}\n />\n <FilterDropdown\n label=\"Status\"\n value={statusFilter}\n onChange={setStatusFilter}\n options={[\n { value: 'all', label: 'All statuses' },\n { value: 'ACTIVE', label: 'Active' },\n { value: 'DRAFT', label: 'Draft' },\n { value: 'PROVISIONING', label: 'Provisioning' },\n { value: 'STOPPED', label: 'Stopped' },\n { value: 'ERROR', label: 'Error' },\n ]}\n />\n </div>\n\n {loading && !data && <LoadingState message=\"Loading tunnels\" />}\n {error && (\n <ErrorState\n title=\"Failed to load tunnels\"\n message={error.message}\n onRetry={() => refetch()}\n />\n )}\n\n {data && tunnels.length === 0 && (\n <EmptyState\n title={tr('vibecontrols.tunnels.list.emptyTitle', 'No tunnels yet')}\n description={tr(\n 'vibecontrols.tunnels.list.emptyDescription',\n 'Create your first tunnel to expose a local service.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => navigate('/vibecontrols/tunnels/new')}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-fg hover:bg-action-primary-bg-hover transition-colors\"\n >\n {tr('vibecontrols.tunnels.list.createTunnel', 'Create tunnel')}\n </button>\n }\n />\n )}\n\n {tunnels.length > 0 && (\n <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className=\"space-y-3\">\n {tunnels.map((tunnel: TunnelRow) => (\n <TunnelCard\n key={tunnel.id}\n tunnel={tunnel}\n isPending={pendingIds.has(tunnel.id)}\n onOpen={(id) => navigate(`/vibecontrols/tunnels/${id}`)}\n onStart={(id) => void handleStartTunnel(id)}\n onStop={(id) => void handleStopTunnel(id)}\n onDelete={(id, name) => setPendingDelete({ id, name })}\n />\n ))}\n </motion.div>\n )}\n\n <ConfirmDialog\n isOpen={pendingDelete !== null}\n onClose={() => setPendingDelete(null)}\n onConfirm={() => {\n if (pendingDelete) {\n void deleteTunnel({ variables: { id: pendingDelete.id } });\n }\n }}\n title=\"Delete tunnel\"\n message={\n pendingDelete\n ? `Delete \"${pendingDelete.name}\"? This will stop the tunnel and remove all its sessions and domains.`\n : ''\n }\n confirmLabel=\"Delete\"\n variant=\"danger\"\n isLoading={deleting}\n />\n\n <QuotaExhaustedDialog\n isOpen={showQuotaExhausted}\n onClose={() => setShowQuotaExhausted(false)}\n onConfirm={() => {\n setShowQuotaExhausted(false);\n navigate('/billing');\n }}\n {...getQuotaDialogContent('tunnel-start')}\n />\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TunnelsTab.js","names":[],"sources":["../../../src/pages/tunnels/TunnelsTab.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { Link, useNavigate } from 'react-router-dom';\nimport { motion } from 'framer-motion';\n\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { PageHeader } from '@/components/shared/PageHeader';\nimport { SearchInput } from '@/components/shared/SearchInput';\nimport { FilterDropdown } from '@/components/shared/FilterDropdown';\nimport { ConfirmDialog } from '@/components/shared/ConfirmDialog';\nimport { QuotaExhaustedDialog } from '@/components/shared/QuotaExhaustedDialog';\nimport { EmptyState } from '@/components/shared/EmptyState';\nimport { LoadingState } from '@/components/shared/LoadingState';\nimport { ErrorState } from '@/components/shared/ErrorState';\nimport { TunnelCard } from '@/components/tunnels/TunnelCard';\nimport type { TunnelStatus, TunnelProviderKey } from '@/generated/wspace-types';\nimport { useTr } from '../../shared/hooks/useTr';\nimport {\n useListTunnelsQuery,\n useListAgentsQuery,\n useStartTunnelMutation,\n useStopTunnelMutation,\n useDeleteTunnelMutation,\n type ListTunnelsQuery,\n} from '@/generated/wspace-operations';\nimport {\n isQuotaExhaustedError,\n markQuotaHandledLocally,\n getQuotaDialogContent,\n} from '@/utils/quotaUtils';\n\ntype TunnelRow = NonNullable<ListTunnelsQuery['tunnels']>['items'][number];\n\nconst TRANSITION_POLL_MS = 4000;\n\nexport function TunnelsTab() {\n const tr = useTr();\n const navigate = useNavigate();\n const [search, setSearch] = useState('');\n const [providerFilter, setProviderFilter] = useState<string>('all');\n const [statusFilter, setStatusFilter] = useState<string>('all');\n const [pendingDelete, setPendingDelete] = useState<{ id: string; name: string } | null>(null);\n const [showQuotaExhausted, setShowQuotaExhausted] = useState(false);\n const [pendingIds, setPendingIds] = useState<Set<string>>(new Set());\n\n const filter = useMemo(() => {\n const f: Record<string, unknown> = {};\n if (providerFilter !== 'all') {\n f.providerKey = providerFilter as TunnelProviderKey;\n }\n if (statusFilter !== 'all') {\n f.status = [statusFilter as TunnelStatus];\n }\n if (search.trim()) {\n f.search = search.trim();\n }\n return f;\n }, [providerFilter, statusFilter, search]);\n\n const { data, loading, error, refetch, startPolling, stopPolling } = useListTunnelsQuery({\n variables: { filter, pagination: { limit: 50 } },\n fetchPolicy: 'cache-and-network',\n });\n\n const { data: agentsData } = useListAgentsQuery({\n variables: { pagination: { limit: 1 } },\n fetchPolicy: 'cache-first',\n });\n const hasAnyAgent = (agentsData?.vibecontrolsAgents?.length ?? 0) > 0;\n\n const tunnels: TunnelRow[] = data?.tunnels?.items ?? [];\n\n const handleNewTunnelClick = (event: React.MouseEvent) => {\n if (!hasAnyAgent) {\n event.preventDefault();\n // Lightweight toast via window event — falls back to console if no host listener.\n const detail = {\n message: tr(\n 'vibecontrols.tunnels.list.createAgentFirst',\n 'Create an agent first to use tunnels'\n ),\n variant: 'info' as const,\n };\n window.dispatchEvent(new CustomEvent('vibecontrols:toast', { detail }));\n navigate('/vibecontrols/agents');\n }\n };\n\n // Poll while any tunnel is in a transient state OR is ACTIVE but has no\n // domain yet (Cloudflare assigns the URL ~5-15 s after start returns).\n const needsPoll =\n pendingIds.size > 0 ||\n tunnels.some(\n (t) =>\n t.status === 'PROVISIONING' ||\n t.status === 'DELETING' ||\n (t.status === 'ACTIVE' && !t.primaryDomain?.hostname)\n );\n useEffect(() => {\n if (needsPoll) {\n startPolling(TRANSITION_POLL_MS);\n } else {\n stopPolling();\n }\n }, [needsPoll, startPolling, stopPolling]);\n\n const addPending = (id: string) => setPendingIds((prev) => new Set([...prev, id]));\n const removePending = (id: string) =>\n setPendingIds((prev) => {\n const next = new Set(prev);\n next.delete(id);\n return next;\n });\n\n const [startTunnel] = useStartTunnelMutation();\n\n const handleStartTunnel = async (id: string) => {\n const existing = tunnels.find((t) => t.id === id);\n if (!existing) return;\n addPending(id);\n try {\n const result = await startTunnel({\n variables: { id },\n optimisticResponse: {\n startTunnel: {\n ...existing,\n status: 'PROVISIONING' as TunnelStatus,\n __typename: 'Tunnel',\n },\n },\n });\n if (result.error && isQuotaExhaustedError(result.error)) {\n markQuotaHandledLocally();\n setShowQuotaExhausted(true);\n }\n } catch (err) {\n // Only open the quota modal for actual quota errors — pre-existing\n // bug spotted on the BOFF-2625 review: any tunnel-start failure\n // (network / validation / auth) was opening the quota dialog.\n if (isQuotaExhaustedError(err)) {\n markQuotaHandledLocally();\n setShowQuotaExhausted(true);\n } else {\n console.error('[TunnelsTab] startTunnel failed', err);\n }\n } finally {\n removePending(id);\n }\n };\n\n const [stopTunnel] = useStopTunnelMutation();\n\n const handleStopTunnel = async (id: string) => {\n const existing = tunnels.find((t) => t.id === id);\n if (!existing) return;\n addPending(id);\n try {\n await stopTunnel({\n variables: { id },\n optimisticResponse: {\n stopTunnel: { ...existing, status: 'STOPPED' as TunnelStatus, __typename: 'Tunnel' },\n },\n });\n } finally {\n removePending(id);\n }\n };\n\n const [deleteTunnel, { loading: deleting }] = useDeleteTunnelMutation({\n onCompleted: () => {\n setPendingDelete(null);\n void refetch();\n },\n });\n\n return (\n <div className=\"p-6 max-w-7xl mx-auto\">\n <PageHeader\n title={tr('vibecontrols.tunnels.list.title', 'Tunnels')}\n description={tr(\n 'vibecontrols.tunnels.list.description',\n 'Expose local services on *.vibetunnels.com and custom domains.'\n )}\n actions={\n <Button variant=\"default\" asChild>\n <Link to=\"/vibecontrols/tunnels/new\" onClick={handleNewTunnelClick}>\n {tr('vibecontrols.tunnels.list.newTunnel', 'New tunnel')}\n </Link>\n </Button>\n }\n />\n\n <div className=\"flex flex-col sm:flex-row gap-3 mb-4\">\n <div className=\"flex-1\">\n <SearchInput\n value={search}\n onChange={setSearch}\n placeholder=\"Search tunnels by name or slug\"\n />\n </div>\n <FilterDropdown\n label=\"Provider\"\n value={providerFilter}\n onChange={setProviderFilter}\n options={[\n { value: 'all', label: 'All providers' },\n { value: 'VIBETUNNELS', label: 'Vibe Tunnels (default)' },\n { value: 'CLOUDFLARE', label: 'Cloudflare Tunnels' },\n ]}\n />\n <FilterDropdown\n label=\"Status\"\n value={statusFilter}\n onChange={setStatusFilter}\n options={[\n { value: 'all', label: 'All statuses' },\n { value: 'ACTIVE', label: 'Active' },\n { value: 'DRAFT', label: 'Draft' },\n { value: 'PROVISIONING', label: 'Provisioning' },\n { value: 'STOPPED', label: 'Stopped' },\n { value: 'ERROR', label: 'Error' },\n ]}\n />\n </div>\n\n {loading && !data && <LoadingState message=\"Loading tunnels\" />}\n {error && (\n <ErrorState\n title=\"Failed to load tunnels\"\n message={error.message}\n onRetry={() => refetch()}\n />\n )}\n\n {data && tunnels.length === 0 && !hasAnyAgent && (\n <EmptyState\n title={tr('vibecontrols.tunnels.list.noAgentTitle', 'Create an agent first')}\n description={tr(\n 'vibecontrols.tunnels.list.noAgentDescription',\n 'Tunnels need a VibeControls agent to forward traffic. Install + register an agent, then come back to create your first tunnel.'\n )}\n action={\n <Button variant=\"default\" onClick={() => navigate('/vibecontrols/agents')}>\n {tr('vibecontrols.tunnels.list.setupAgent', 'Set up an agent')}\n </Button>\n }\n />\n )}\n\n {data && tunnels.length === 0 && hasAnyAgent && (\n <EmptyState\n title={tr('vibecontrols.tunnels.list.emptyTitle', 'No tunnels yet')}\n description={tr(\n 'vibecontrols.tunnels.list.emptyDescription',\n 'Create your first tunnel to expose a local service.'\n )}\n action={\n <Button variant=\"default\" onClick={() => navigate('/vibecontrols/tunnels/new')}>\n {tr('vibecontrols.tunnels.list.createTunnel', 'Create tunnel')}\n </Button>\n }\n />\n )}\n\n {tunnels.length > 0 && (\n <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className=\"space-y-3\">\n {tunnels.map((tunnel: TunnelRow) => (\n <TunnelCard\n key={tunnel.id}\n tunnel={tunnel}\n isPending={pendingIds.has(tunnel.id)}\n onOpen={(id) => navigate(`/vibecontrols/tunnels/${id}`)}\n onStart={(id) => void handleStartTunnel(id)}\n onStop={(id) => void handleStopTunnel(id)}\n onDelete={(id, name) => setPendingDelete({ id, name })}\n />\n ))}\n </motion.div>\n )}\n\n <ConfirmDialog\n isOpen={pendingDelete !== null}\n onClose={() => setPendingDelete(null)}\n onConfirm={() => {\n if (pendingDelete) {\n void deleteTunnel({ variables: { id: pendingDelete.id } });\n }\n }}\n title=\"Delete tunnel\"\n message={\n pendingDelete\n ? `Delete \"${pendingDelete.name}\"? This will stop the tunnel and remove all its sessions and domains.`\n : ''\n }\n confirmLabel=\"Delete\"\n variant=\"danger\"\n isLoading={deleting}\n />\n\n <QuotaExhaustedDialog\n isOpen={showQuotaExhausted}\n onClose={() => setShowQuotaExhausted(false)}\n onConfirm={() => {\n setShowQuotaExhausted(false);\n navigate('/billing');\n }}\n {...getQuotaDialogContent('tunnel-start')}\n />\n </div>\n );\n}\n"],"mappings":"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"}
@@ -4,9 +4,40 @@ import { graphqlFetch as t } from "@burdenoff/fe-libs/shared/graphql/fetch";
4
4
  function n() {
5
5
  return e() ?? "";
6
6
  }
7
- async function r(e) {
7
+ function r() {
8
+ try {
9
+ return !1;
10
+ } catch {
11
+ return !1;
12
+ }
13
+ }
14
+ async function i(e) {
8
15
  e.workspaceId || n();
9
- let r = await t({
16
+ let i = r() && e.agentRef ? {
17
+ executorType: "GRAPHQL",
18
+ executorConfig: {
19
+ operation: "executeScheduledPrompt",
20
+ variables: {
21
+ agentId: e.agentRef.agentId,
22
+ profile: e.agentRef.profile,
23
+ sessionId: e.sessionId,
24
+ prompt: e.prompt
25
+ }
26
+ }
27
+ } : {
28
+ executorType: "WEBHOOK",
29
+ executorConfig: {
30
+ url: `${e.agentTunnelUrl}/api/ai/sessions/${e.sessionId}/send`,
31
+ method: "POST",
32
+ headers: {
33
+ "Content-Type": "application/json",
34
+ ...e.agentApiKey ? { "X-API-Key": e.agentApiKey } : {}
35
+ },
36
+ body: JSON.stringify({ prompt: e.prompt })
37
+ }
38
+ };
39
+ if (i.executorType === "WEBHOOK" && !e.agentApiKey) throw Error("schedulePrompt: legacy WEBHOOK executor requires agentApiKey. Set VITE_SCHEDULER_AGENT_REF_ENABLED=true and pass agentRef to use the secure path.");
40
+ let a = await t({
10
41
  gateway: "workspace",
11
42
  query: "mutation CreateScheduledJob($input: CreateScheduledJobInput!) {\n createScheduledJob(input: $input) { id status }\n }",
12
43
  variables: { input: {
@@ -15,26 +46,17 @@ async function r(e) {
15
46
  scheduleType: "ONE_TIME",
16
47
  startDate: e.scheduledAt,
17
48
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
18
- executorType: "WEBHOOK",
19
- executorConfig: {
20
- url: `${e.agentTunnelUrl}/api/ai/sessions/${e.sessionId}/send`,
21
- method: "POST",
22
- headers: {
23
- "Content-Type": "application/json",
24
- "X-API-Key": e.agentApiKey
25
- },
26
- body: JSON.stringify({ prompt: e.prompt })
27
- },
49
+ ...i,
28
50
  isActive: !0
29
51
  } }
30
52
  });
31
- if (r.errors?.length) throw Error(r.errors[0].message);
53
+ if (a.errors?.length) throw Error(a.errors[0].message);
32
54
  return {
33
- jobId: r.data?.createScheduledJob?.id || "",
55
+ jobId: a.data?.createScheduledJob?.id || "",
34
56
  scheduledAt: e.scheduledAt
35
57
  };
36
58
  }
37
59
  //#endregion
38
- export { r as schedulePrompt };
60
+ export { i as schedulePrompt };
39
61
 
40
62
  //# sourceMappingURL=schedulerApi.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"schedulerApi.js","names":[],"sources":["../../src/services/schedulerApi.ts"],"sourcesContent":["/**\n * Scheduler API service for scheduling AI prompts.\n *\n * Creates one-time scheduled jobs via the workspace scheduler\n * that fire webhook calls to the AI session send endpoint.\n */\n\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql/fetch';\nimport { getStoredWorkspaceId } from '@burdenoff/fe-libs/shared/graphql';\n\n// Reads from ActiveContextStorage with URL precedence so the workspace id\n// is found even when the calling MFE route doesn't carry `?workspace=`.\nfunction getWorkspaceId(): string {\n return getStoredWorkspaceId() ?? '';\n}\n\nexport async function schedulePrompt(params: {\n sessionId: string;\n agentTunnelUrl: string;\n agentApiKey: string;\n prompt: string;\n scheduledAt: string; // ISO datetime\n workspaceId?: string;\n}): Promise<{ jobId: string; scheduledAt: string }> {\n const wsId = params.workspaceId || getWorkspaceId();\n\n const result = await graphqlFetch<{\n createScheduledJob?: { id?: string; status?: string };\n }>({\n gateway: 'workspace',\n query: `mutation CreateScheduledJob($input: CreateScheduledJobInput!) {\n createScheduledJob(input: $input) { id status }\n }`,\n variables: {\n input: {\n name: `AI Prompt - ${params.sessionId.slice(0, 8)}`,\n description: `Scheduled prompt for AI session`,\n scheduleType: 'ONE_TIME',\n startDate: params.scheduledAt,\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n executorType: 'WEBHOOK',\n executorConfig: {\n url: `${params.agentTunnelUrl}/api/ai/sessions/${params.sessionId}/send`,\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-API-Key': params.agentApiKey },\n body: JSON.stringify({ prompt: params.prompt }),\n },\n isActive: true,\n },\n },\n });\n\n if (result.errors?.length) throw new Error(result.errors[0].message);\n const job = result.data?.createScheduledJob;\n return { jobId: job?.id || '', scheduledAt: params.scheduledAt };\n}\n"],"mappings":";;;AAYA,SAAS,IAAyB;AAChC,QAAO,GAAsB,IAAI;;AAGnC,eAAsB,EAAe,GAOe;AACrC,GAAO,eAAe,GAAgB;CAEnD,IAAM,IAAS,MAAM,EAElB;EACD,SAAS;EACT,OAAO;EAGP,WAAW,EACT,OAAO;GACL,MAAM,eAAe,EAAO,UAAU,MAAM,GAAG,EAAE;GACjD,aAAa;GACb,cAAc;GACd,WAAW,EAAO;GAClB,UAAU,KAAK,gBAAgB,CAAC,iBAAiB,CAAC;GAClD,cAAc;GACd,gBAAgB;IACd,KAAK,GAAG,EAAO,eAAe,mBAAmB,EAAO,UAAU;IAClE,QAAQ;IACR,SAAS;KAAE,gBAAgB;KAAoB,aAAa,EAAO;KAAa;IAChF,MAAM,KAAK,UAAU,EAAE,QAAQ,EAAO,QAAQ,CAAC;IAChD;GACD,UAAU;GACX,EACF;EACF,CAAC;AAEF,KAAI,EAAO,QAAQ,OAAQ,OAAU,MAAM,EAAO,OAAO,GAAG,QAAQ;AAEpE,QAAO;EAAE,OADG,EAAO,MAAM,oBACJ,MAAM;EAAI,aAAa,EAAO;EAAa"}
1
+ {"version":3,"file":"schedulerApi.js","names":[],"sources":["../../src/services/schedulerApi.ts"],"sourcesContent":["/**\n * Scheduler API service for scheduling AI prompts.\n *\n * Creates one-time scheduled jobs via the workspace scheduler\n * that fire webhook calls to the AI session send endpoint.\n *\n * SECURITY-F12 (2026-05-28): The legacy WEBHOOK path persists the agent's\n * long-lived `agentApiKey` in `executorConfig.headers['X-API-Key']` inside\n * the scheduler DB. New code paths should instead send `agentRef:{agentId,\n * profile}` so the scheduler invokes a backend mutation that mints a\n * short-lived (≤60s) ticket at execution time. See\n * `~/products/dev/platform/context/scheduler/SECURITY_F12.md` for the full\n * migration plan and why phases 2-4 are deferred.\n *\n * This file ships the microfe half (phase 1) behind a feature flag. With\n * the flag OFF (default) the legacy webhook payload is preserved so\n * running scheduled jobs continue to fire. With the flag ON AND `agentRef`\n * supplied, the secret-free GRAPHQL executor payload is emitted instead.\n */\n\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql/fetch';\nimport { getStoredWorkspaceId } from '@burdenoff/fe-libs/shared/graphql';\n\n// Reads from ActiveContextStorage with URL precedence so the workspace id\n// is found even when the calling MFE route doesn't carry `?workspace=`.\nfunction getWorkspaceId(): string {\n return getStoredWorkspaceId() ?? '';\n}\n\nfunction isAgentRefEnabled(): boolean {\n // Vite-injected env var. Default off — the backend (phase 2 + 3) isn't\n // shipped yet, flipping this without the backend would break new\n // scheduled jobs the moment they fire.\n try {\n return import.meta.env?.VITE_SCHEDULER_AGENT_REF_ENABLED === 'true';\n } catch {\n return false;\n }\n}\n\nexport interface AgentRef {\n agentId: string;\n profile: string;\n}\n\nexport async function schedulePrompt(params: {\n sessionId: string;\n agentTunnelUrl: string;\n /**\n * Legacy long-lived agent API key. SECURITY-F12: do not persist in new\n * executor configs once the `agentRef` path is enabled. Still accepted\n * here so existing call sites and feature-flag-off deployments work.\n */\n agentApiKey?: string;\n /**\n * Preferred (phase 1+): reference the agent without embedding its key.\n * Backend mints a short-lived ticket at execution time.\n */\n agentRef?: AgentRef;\n prompt: string;\n scheduledAt: string; // ISO datetime\n workspaceId?: string;\n}): Promise<{ jobId: string; scheduledAt: string }> {\n const wsId = params.workspaceId || getWorkspaceId();\n\n const useAgentRef = isAgentRefEnabled() && !!params.agentRef;\n\n // SECURITY-F12 phase 1: when the flag is on AND we have an agentRef,\n // emit the secret-free GRAPHQL executor payload. The scheduler will\n // dispatch this through wspace-int-gateway to the (not-yet-shipped)\n // `executeScheduledPrompt` mutation on wspace-vibecontrols-svc, which\n // mints a short-lived ticket at execution time.\n const executor = useAgentRef\n ? {\n executorType: 'GRAPHQL' as const,\n executorConfig: {\n operation: 'executeScheduledPrompt',\n variables: {\n agentId: params.agentRef!.agentId,\n profile: params.agentRef!.profile,\n sessionId: params.sessionId,\n prompt: params.prompt,\n },\n },\n }\n : {\n // Legacy path: WEBHOOK with the raw long-lived `agentApiKey`\n // embedded in headers. Kept for backwards compatibility until\n // SECURITY-F12 phase 2 + 3 ship and this branch is removed in\n // phase 4. Refuses to schedule without a key (the legacy\n // executor cannot authenticate against the agent without one).\n executorType: 'WEBHOOK' as const,\n executorConfig: {\n url: `${params.agentTunnelUrl}/api/ai/sessions/${params.sessionId}/send`,\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(params.agentApiKey ? { 'X-API-Key': params.agentApiKey } : {}),\n },\n body: JSON.stringify({ prompt: params.prompt }),\n },\n };\n\n if (executor.executorType === 'WEBHOOK' && !params.agentApiKey) {\n throw new Error(\n 'schedulePrompt: legacy WEBHOOK executor requires agentApiKey. Set VITE_SCHEDULER_AGENT_REF_ENABLED=true and pass agentRef to use the secure path.'\n );\n }\n\n const result = await graphqlFetch<{\n createScheduledJob?: { id?: string; status?: string };\n }>({\n gateway: 'workspace',\n query: `mutation CreateScheduledJob($input: CreateScheduledJobInput!) {\n createScheduledJob(input: $input) { id status }\n }`,\n variables: {\n input: {\n name: `AI Prompt - ${params.sessionId.slice(0, 8)}`,\n description: `Scheduled prompt for AI session`,\n scheduleType: 'ONE_TIME',\n startDate: params.scheduledAt,\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n ...executor,\n isActive: true,\n },\n },\n });\n\n if (result.errors?.length) throw new Error(result.errors[0].message);\n const job = result.data?.createScheduledJob;\n return { jobId: job?.id || '', scheduledAt: params.scheduledAt };\n}\n"],"mappings":";;;AAyBA,SAAS,IAAyB;AAChC,QAAO,GAAsB,IAAI;;AAGnC,SAAS,IAA6B;AAIpC,KAAI;AACF,SAAO;SACD;AACN,SAAO;;;AASX,eAAsB,EAAe,GAiBe;AACrC,GAAO,eAAe,GAAgB;CASnD,IAAM,IAPc,GAAmB,IAAM,EAAO,WAQhD;EACE,cAAc;EACd,gBAAgB;GACd,WAAW;GACX,WAAW;IACT,SAAS,EAAO,SAAU;IAC1B,SAAS,EAAO,SAAU;IAC1B,WAAW,EAAO;IAClB,QAAQ,EAAO;IAChB;GACF;EACF,GACD;EAME,cAAc;EACd,gBAAgB;GACd,KAAK,GAAG,EAAO,eAAe,mBAAmB,EAAO,UAAU;GAClE,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,GAAI,EAAO,cAAc,EAAE,aAAa,EAAO,aAAa,GAAG,EAAE;IAClE;GACD,MAAM,KAAK,UAAU,EAAE,QAAQ,EAAO,QAAQ,CAAC;GAChD;EACF;AAEL,KAAI,EAAS,iBAAiB,aAAa,CAAC,EAAO,YACjD,OAAU,MACR,oJACD;CAGH,IAAM,IAAS,MAAM,EAElB;EACD,SAAS;EACT,OAAO;EAGP,WAAW,EACT,OAAO;GACL,MAAM,eAAe,EAAO,UAAU,MAAM,GAAG,EAAE;GACjD,aAAa;GACb,cAAc;GACd,WAAW,EAAO;GAClB,UAAU,KAAK,gBAAgB,CAAC,iBAAiB,CAAC;GAClD,GAAG;GACH,UAAU;GACX,EACF;EACF,CAAC;AAEF,KAAI,EAAO,QAAQ,OAAQ,OAAU,MAAM,EAAO,OAAO,GAAG,QAAQ;AAEpE,QAAO;EAAE,OADG,EAAO,MAAM,oBACJ,MAAM;EAAI,aAAa,EAAO;EAAa"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-vibecontrols",
3
- "version": "2026.528.7",
3
+ "version": "2026.528.9",
4
4
  "description": "VibeControls microfrontend for Burdenoff products",
5
5
  "type": "module",
6
6
  "files": [