orionfold-relay 0.25.0 → 0.26.0
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/dist/cli.js +36 -12
- package/package.json +1 -1
- package/src/app/api/chat/conversations/route.ts +4 -1
- package/src/app/api/data/clear/route.ts +8 -1
- package/src/app/api/data/seed/route.ts +8 -1
- package/src/app/settings/page.tsx +3 -1
- package/src/components/apps/kit-view/slots/header.tsx +9 -3
- package/src/components/apps/run-now-button.tsx +7 -3
- package/src/components/apps/run-now-sheet.tsx +6 -2
- package/src/components/apps/run-now-toast.ts +27 -0
- package/src/components/chat/chat-session-provider.tsx +10 -1
- package/src/components/settings/data-management-section.tsx +56 -9
- package/src/lib/apps/view-kits/data.ts +12 -1
- package/src/lib/apps/view-kits/header-status.ts +14 -0
- package/src/lib/apps/view-kits/kits/coach.ts +2 -1
- package/src/lib/apps/view-kits/kits/inbox.ts +2 -1
- package/src/lib/apps/view-kits/kits/ledger.ts +2 -1
- package/src/lib/apps/view-kits/kits/placeholder.ts +2 -1
- package/src/lib/apps/view-kits/kits/research.ts +2 -1
- package/src/lib/apps/view-kits/kits/tracker.ts +2 -1
- package/src/lib/apps/view-kits/kits/workflow-hub.ts +2 -1
- package/src/lib/apps/view-kits/types.ts +7 -1
- package/src/lib/chat/files/search.ts +5 -0
- package/src/lib/chat/tools/table-tools.ts +5 -2
- package/src/lib/constants/status-families.ts +13 -1
- package/src/lib/environment/git-manager.ts +5 -0
- package/src/lib/http/self-base-url.ts +42 -0
- package/src/lib/packs/install.ts +19 -0
- package/src/lib/plugins/examples/echo-server/plugin.yaml +1 -1
- package/src/lib/plugins/examples/finance-pack/plugin.yaml +1 -1
- package/src/lib/plugins/examples/reading-radar/plugin.yaml +1 -1
- package/src/lib/plugins/registry.ts +1 -1
- package/src/lib/plugins/sdk/types.ts +1 -1
- package/src/lib/tables/trigger-evaluator.ts +6 -6
package/dist/cli.js
CHANGED
|
@@ -1186,7 +1186,7 @@ var CURRENT_PLUGIN_API_VERSION, CAPABILITY_VALUES, ORIGIN_VALUES, PrimitivesBund
|
|
|
1186
1186
|
var init_types = __esm({
|
|
1187
1187
|
"src/lib/plugins/sdk/types.ts"() {
|
|
1188
1188
|
"use strict";
|
|
1189
|
-
CURRENT_PLUGIN_API_VERSION = "0.
|
|
1189
|
+
CURRENT_PLUGIN_API_VERSION = "0.26";
|
|
1190
1190
|
CAPABILITY_VALUES = ["fs", "net", "child_process", "env"];
|
|
1191
1191
|
ORIGIN_VALUES = ["ainative-internal", "third-party"];
|
|
1192
1192
|
PrimitivesBundleManifestSchema = z.object({
|
|
@@ -4614,6 +4614,19 @@ var init_history = __esm({
|
|
|
4614
4614
|
}
|
|
4615
4615
|
});
|
|
4616
4616
|
|
|
4617
|
+
// src/lib/http/self-base-url.ts
|
|
4618
|
+
function getSelfBaseUrl() {
|
|
4619
|
+
const explicit = process.env.RELAY_SELF_BASE_URL || process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL;
|
|
4620
|
+
if (explicit) return explicit;
|
|
4621
|
+
const port = process.env.PORT || "3000";
|
|
4622
|
+
return `http://127.0.0.1:${port}`;
|
|
4623
|
+
}
|
|
4624
|
+
var init_self_base_url = __esm({
|
|
4625
|
+
"src/lib/http/self-base-url.ts"() {
|
|
4626
|
+
"use strict";
|
|
4627
|
+
}
|
|
4628
|
+
});
|
|
4629
|
+
|
|
4617
4630
|
// src/lib/workflows/delay.ts
|
|
4618
4631
|
function parseDuration(input) {
|
|
4619
4632
|
const match = input.match(DURATION_PATTERN);
|
|
@@ -12969,7 +12982,7 @@ var init_registry6 = __esm({
|
|
|
12969
12982
|
init_registry5();
|
|
12970
12983
|
init_installer();
|
|
12971
12984
|
init_schedule_spec();
|
|
12972
|
-
SUPPORTED_API_VERSIONS = /* @__PURE__ */ new Set([CURRENT_PLUGIN_API_VERSION, "0.
|
|
12985
|
+
SUPPORTED_API_VERSIONS = /* @__PURE__ */ new Set([CURRENT_PLUGIN_API_VERSION, "0.25"]);
|
|
12973
12986
|
pluginCache = null;
|
|
12974
12987
|
lastLoadedPluginIds = /* @__PURE__ */ new Set();
|
|
12975
12988
|
PluginTableSchema = z16.object({
|
|
@@ -15281,7 +15294,7 @@ Guidelines for schema inference:
|
|
|
15281
15294
|
try {
|
|
15282
15295
|
const table = await getTable(args.tableId);
|
|
15283
15296
|
if (!table) return err("Table not found");
|
|
15284
|
-
const baseUrl =
|
|
15297
|
+
const baseUrl = getSelfBaseUrl();
|
|
15285
15298
|
return ok({
|
|
15286
15299
|
url: `${baseUrl}/api/tables/${args.tableId}/export?format=${args.format}`,
|
|
15287
15300
|
table: table.name,
|
|
@@ -15727,7 +15740,7 @@ Guidelines for schema inference:
|
|
|
15727
15740
|
];
|
|
15728
15741
|
}
|
|
15729
15742
|
function getBaseUrl() {
|
|
15730
|
-
return
|
|
15743
|
+
return getSelfBaseUrl();
|
|
15731
15744
|
}
|
|
15732
15745
|
var init_table_tools = __esm({
|
|
15733
15746
|
"src/lib/chat/tools/table-tools.ts"() {
|
|
@@ -15738,6 +15751,7 @@ var init_table_tools = __esm({
|
|
|
15738
15751
|
init_history();
|
|
15739
15752
|
init_import();
|
|
15740
15753
|
init_enrichment();
|
|
15754
|
+
init_self_base_url();
|
|
15741
15755
|
}
|
|
15742
15756
|
});
|
|
15743
15757
|
|
|
@@ -25379,13 +25393,15 @@ async function fireAction(actionType, config, rowData, meta) {
|
|
|
25379
25393
|
Trigger data: ${JSON.stringify(rowData, null, 2)}` : `Triggered by table row change.
|
|
25380
25394
|
|
|
25381
25395
|
Data: ${JSON.stringify(rowData, null, 2)}`;
|
|
25382
|
-
await fetch(`${
|
|
25396
|
+
await fetch(`${getSelfBaseUrl()}/api/tasks`, {
|
|
25383
25397
|
method: "POST",
|
|
25384
25398
|
headers: { "Content-Type": "application/json" },
|
|
25385
25399
|
body: JSON.stringify({
|
|
25386
25400
|
title: config.title ?? "Triggered Task",
|
|
25387
25401
|
description,
|
|
25388
|
-
|
|
25402
|
+
// createTaskSchema wants `string | undefined`, not null — sending null
|
|
25403
|
+
// 400s the self-call and the task is never created. Omit when absent.
|
|
25404
|
+
...config.projectId ? { projectId: config.projectId } : {}
|
|
25389
25405
|
})
|
|
25390
25406
|
});
|
|
25391
25407
|
return;
|
|
@@ -25411,7 +25427,7 @@ Data: ${JSON.stringify(rowData, null, 2)}`;
|
|
|
25411
25427
|
return;
|
|
25412
25428
|
}
|
|
25413
25429
|
if (actionType === "run_workflow" && config.workflowId) {
|
|
25414
|
-
await fetch(`${
|
|
25430
|
+
await fetch(`${getSelfBaseUrl()}/api/workflows/${config.workflowId}/execute`, {
|
|
25415
25431
|
method: "POST",
|
|
25416
25432
|
headers: { "Content-Type": "application/json" },
|
|
25417
25433
|
body: JSON.stringify({
|
|
@@ -25429,14 +25445,12 @@ function deriveAppIdFromBlueprintId(blueprintId) {
|
|
|
25429
25445
|
if (idx <= 0) return null;
|
|
25430
25446
|
return blueprintId.slice(0, idx);
|
|
25431
25447
|
}
|
|
25432
|
-
function getBaseUrl2() {
|
|
25433
|
-
return process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
|
|
25434
|
-
}
|
|
25435
25448
|
var init_trigger_evaluator = __esm({
|
|
25436
25449
|
"src/lib/tables/trigger-evaluator.ts"() {
|
|
25437
25450
|
"use strict";
|
|
25438
25451
|
init_db();
|
|
25439
25452
|
init_schema();
|
|
25453
|
+
init_self_base_url();
|
|
25440
25454
|
}
|
|
25441
25455
|
});
|
|
25442
25456
|
|
|
@@ -25899,8 +25913,8 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
25899
25913
|
import yaml12 from "js-yaml";
|
|
25900
25914
|
import semver from "semver";
|
|
25901
25915
|
function relayCoreVersion() {
|
|
25902
|
-
if (semver.valid("0.
|
|
25903
|
-
return "0.
|
|
25916
|
+
if (semver.valid("0.26.0")) {
|
|
25917
|
+
return "0.26.0";
|
|
25904
25918
|
}
|
|
25905
25919
|
try {
|
|
25906
25920
|
const root = getAppRoot(import.meta.dirname, 3);
|
|
@@ -26078,6 +26092,11 @@ async function installPack(source, options = {}) {
|
|
|
26078
26092
|
const { reloadBlueprints: reloadBlueprints2 } = await Promise.resolve().then(() => (init_registry3(), registry_exports3));
|
|
26079
26093
|
reloadBlueprints2();
|
|
26080
26094
|
}
|
|
26095
|
+
try {
|
|
26096
|
+
const { revalidateTag } = await import("next/cache");
|
|
26097
|
+
revalidateTag(`app-runtime:${pack.meta.id}`, { expire: 0 });
|
|
26098
|
+
} catch {
|
|
26099
|
+
}
|
|
26081
26100
|
return {
|
|
26082
26101
|
packId: pack.meta.id,
|
|
26083
26102
|
packVersion: pack.meta.version,
|
|
@@ -27548,6 +27567,11 @@ Falling back to development mode for this run \u2014 Relay still works, but slow
|
|
|
27548
27567
|
RELAY_DATA_DIR: DATA_DIR,
|
|
27549
27568
|
RELAY_LAUNCH_CWD: launchCwd2,
|
|
27550
27569
|
PORT: String(actualPort),
|
|
27570
|
+
// Origin for Relay's internal loopback self-calls (trigger dispatch,
|
|
27571
|
+
// compose table tools). The server always listens on loopback even when
|
|
27572
|
+
// bound to a non-loopback host, so self-calls target 127.0.0.1 + the real
|
|
27573
|
+
// port — never :3000, never the LAN IP. Fixes issue #29.
|
|
27574
|
+
RELAY_SELF_BASE_URL: buildSidecarUrl(actualPort, "127.0.0.1"),
|
|
27551
27575
|
...opts.safeMode ? { RELAY_SAFE_MODE: "true" } : {},
|
|
27552
27576
|
// In dev mode, Next blocks cross-origin /_next/* dev-asset requests from
|
|
27553
27577
|
// the LAN client's IP, breaking the app over the network (issue #13).
|
package/package.json
CHANGED
|
@@ -51,7 +51,10 @@ export async function POST(req: NextRequest) {
|
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
// "ollama" is first-class: getRuntimeForModel() returns it for local models,
|
|
55
|
+
// engine.ts routes it to sendOllamaMessage. Omitting it here 400'd the
|
|
56
|
+
// "Best privacy (local only)" tier's first chat/compose on a fresh install (#30).
|
|
57
|
+
const validRuntimes = ["claude-code", "openai-codex-app-server", "ollama"];
|
|
55
58
|
if (!validRuntimes.includes(runtimeId)) {
|
|
56
59
|
return NextResponse.json(
|
|
57
60
|
{ error: `Invalid runtimeId. Must be one of: ${validRuntimes.join(", ")}` },
|
|
@@ -4,7 +4,14 @@ import { isDataOpsAllowed } from "@/lib/data/staging-gate";
|
|
|
4
4
|
|
|
5
5
|
export async function POST() {
|
|
6
6
|
if (!isDataOpsAllowed()) {
|
|
7
|
-
return NextResponse.json(
|
|
7
|
+
return NextResponse.json(
|
|
8
|
+
{
|
|
9
|
+
success: false,
|
|
10
|
+
error:
|
|
11
|
+
"Clearing data is a staging-only tool and is disabled on this build.",
|
|
12
|
+
},
|
|
13
|
+
{ status: 403 }
|
|
14
|
+
);
|
|
8
15
|
}
|
|
9
16
|
|
|
10
17
|
try {
|
|
@@ -4,7 +4,14 @@ import { isDataOpsAllowed } from "@/lib/data/staging-gate";
|
|
|
4
4
|
|
|
5
5
|
export async function POST() {
|
|
6
6
|
if (!isDataOpsAllowed()) {
|
|
7
|
-
return NextResponse.json(
|
|
7
|
+
return NextResponse.json(
|
|
8
|
+
{
|
|
9
|
+
success: false,
|
|
10
|
+
error:
|
|
11
|
+
"Sample data seeding is a staging-only tool and is disabled on this build.",
|
|
12
|
+
},
|
|
13
|
+
{ status: 403 }
|
|
14
|
+
);
|
|
8
15
|
}
|
|
9
16
|
|
|
10
17
|
try {
|
|
@@ -14,10 +14,12 @@ import { ChannelsSection } from "@/components/settings/channels-section";
|
|
|
14
14
|
import { InstanceSection } from "@/components/instance/instance-section";
|
|
15
15
|
import { LicenseSection } from "@/components/settings/license-section";
|
|
16
16
|
import { PageShell } from "@/components/shared/page-shell";
|
|
17
|
+
import { isDataOpsAllowed } from "@/lib/data/staging-gate";
|
|
17
18
|
|
|
18
19
|
export const dynamic = "force-dynamic";
|
|
19
20
|
|
|
20
21
|
export default function SettingsPage() {
|
|
22
|
+
const dataOpsAllowed = isDataOpsAllowed();
|
|
21
23
|
return (
|
|
22
24
|
<PageShell
|
|
23
25
|
title="Settings"
|
|
@@ -38,7 +40,7 @@ export default function SettingsPage() {
|
|
|
38
40
|
<BudgetGuardrailsSection />
|
|
39
41
|
<PermissionsSections />
|
|
40
42
|
<DatabaseSnapshotsSection />
|
|
41
|
-
<DataManagementSection />
|
|
43
|
+
<DataManagementSection allowed={dataOpsAllowed} />
|
|
42
44
|
</div>
|
|
43
45
|
</PageShell>
|
|
44
46
|
);
|
|
@@ -22,8 +22,8 @@ interface HeaderSlotProps {
|
|
|
22
22
|
export function HeaderSlotView({ slot, manifestPane }: HeaderSlotProps) {
|
|
23
23
|
const { title, description, status, actions, cadenceChip, runNowBlueprintId, runNowVariables, periodChip, triggerSourceChip } = slot;
|
|
24
24
|
return (
|
|
25
|
-
<div className="flex flex-col gap-
|
|
26
|
-
<div className="min-w-0 flex-1">
|
|
25
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between sm:gap-x-4 sm:gap-y-2">
|
|
26
|
+
<div className="min-w-0 flex-1 sm:min-w-[16rem]">
|
|
27
27
|
<h1 className="text-xl font-semibold tracking-tight line-clamp-2" title={title}>
|
|
28
28
|
{title}
|
|
29
29
|
</h1>
|
|
@@ -33,7 +33,13 @@ export function HeaderSlotView({ slot, manifestPane }: HeaderSlotProps) {
|
|
|
33
33
|
</p>
|
|
34
34
|
)}
|
|
35
35
|
</div>
|
|
36
|
-
|
|
36
|
+
{/*
|
|
37
|
+
* Action group stays a single non-wrapping row (chips + Run + manifest
|
|
38
|
+
* never split across lines). When the title can't yield enough inline
|
|
39
|
+
* space, the PARENT's flex-wrap drops this whole group to its own row —
|
|
40
|
+
* an intentional stack, not an accidental two-line break (FEAT-4).
|
|
41
|
+
*/}
|
|
42
|
+
<div className="flex flex-nowrap items-center gap-2 sm:shrink-0">
|
|
37
43
|
{status && <StatusChip status={status} size="md" />}
|
|
38
44
|
{cadenceChip && (
|
|
39
45
|
<ScheduleCadenceChip
|
|
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
|
|
|
5
5
|
import { Play } from "lucide-react";
|
|
6
6
|
import { toast } from "sonner";
|
|
7
7
|
import { RunNowSheet } from "./run-now-sheet";
|
|
8
|
+
import { toastDraftCreated } from "./run-now-toast";
|
|
8
9
|
import type { BlueprintVariable } from "@/lib/workflows/blueprints/types";
|
|
9
10
|
|
|
10
11
|
interface RunNowButtonProps {
|
|
@@ -63,12 +64,15 @@ export function RunNowButton({
|
|
|
63
64
|
});
|
|
64
65
|
if (!res.ok) {
|
|
65
66
|
const err = (await res.json().catch(() => ({}))) as { error?: string };
|
|
66
|
-
toast.error(err.error ?? `Failed to
|
|
67
|
+
toast.error(err.error ?? `Failed to create draft (${res.status})`);
|
|
67
68
|
return;
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
+
const body = (await res.json().catch(() => ({}))) as {
|
|
71
|
+
workflowId?: string;
|
|
72
|
+
};
|
|
73
|
+
toastDraftCreated(body.workflowId);
|
|
70
74
|
} catch (err) {
|
|
71
|
-
toast.error(err instanceof Error ? err.message : "
|
|
75
|
+
toast.error(err instanceof Error ? err.message : "Could not create draft");
|
|
72
76
|
} finally {
|
|
73
77
|
setPending(false);
|
|
74
78
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "@/components/ui/sheet";
|
|
14
14
|
import { VariableInput } from "@/components/workflows/variable-input";
|
|
15
15
|
import { validateVariables } from "@/lib/workflows/blueprints/validate-variables";
|
|
16
|
+
import { toastDraftCreated } from "./run-now-toast";
|
|
16
17
|
import type { BlueprintVariable } from "@/lib/workflows/blueprints/types";
|
|
17
18
|
|
|
18
19
|
interface RunNowSheetProps {
|
|
@@ -74,10 +75,13 @@ export function RunNowSheet({
|
|
|
74
75
|
}
|
|
75
76
|
return;
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
+
const body = (await res.json().catch(() => ({}))) as {
|
|
79
|
+
workflowId?: string;
|
|
80
|
+
};
|
|
81
|
+
toastDraftCreated(body.workflowId);
|
|
78
82
|
setOpen(false);
|
|
79
83
|
} catch (err) {
|
|
80
|
-
toast.error(err instanceof Error ? err.message : "
|
|
84
|
+
toast.error(err instanceof Error ? err.message : "Could not create draft");
|
|
81
85
|
} finally {
|
|
82
86
|
setPending(false);
|
|
83
87
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { toast } from "sonner";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BUG-4: "Run now" POSTs the blueprint instantiate endpoint, which creates a
|
|
5
|
+
* `draft` workflow and does NOT dispatch it. The old toast said "Run started",
|
|
6
|
+
* which is a lie — nothing runs until the user hits Execute on the workflow
|
|
7
|
+
* detail page. Surface the truth: a draft was created, and deep-link to where
|
|
8
|
+
* it can be executed.
|
|
9
|
+
*
|
|
10
|
+
* Shared by `run-now-button.tsx` (direct POST) and `run-now-sheet.tsx`
|
|
11
|
+
* (variable path) so the honest copy can't drift between the two.
|
|
12
|
+
*/
|
|
13
|
+
export function toastDraftCreated(workflowId: string | undefined): void {
|
|
14
|
+
if (!workflowId) {
|
|
15
|
+
// No id to link to — still don't claim it started.
|
|
16
|
+
toast.success("Draft created. Open it in Workflows to Execute.");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
toast.success("Draft created. Open it in Workflows to Execute.", {
|
|
20
|
+
action: {
|
|
21
|
+
label: "Open workflow",
|
|
22
|
+
onClick: () => {
|
|
23
|
+
window.location.assign(`/workflows/${workflowId}`);
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -297,7 +297,16 @@ export function ChatSessionProvider({ children }: { children: ReactNode }) {
|
|
|
297
297
|
...(opts?.title ? { title: opts.title } : {}),
|
|
298
298
|
}),
|
|
299
299
|
});
|
|
300
|
-
if (!res.ok)
|
|
300
|
+
if (!res.ok) {
|
|
301
|
+
// Never swallow a create failure silently again (#30): a rejected
|
|
302
|
+
// runtime/model used to clear the composer with no signal at all.
|
|
303
|
+
const detail = await res
|
|
304
|
+
.json()
|
|
305
|
+
.then((b) => (b as { error?: string })?.error)
|
|
306
|
+
.catch(() => null);
|
|
307
|
+
toast.error(detail || "Couldn't start a chat with this model. Try another model.");
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
301
310
|
const conversation = (await res.json()) as ConversationRow;
|
|
302
311
|
setConversations((prev) => [conversation, ...prev]);
|
|
303
312
|
// Set empty messages BEFORE activating so the conversation has an
|
|
@@ -15,7 +15,35 @@ import { ConfirmDialog } from "@/components/shared/confirm-dialog";
|
|
|
15
15
|
import { toast } from "sonner";
|
|
16
16
|
import { Loader2, Trash2, Database } from "lucide-react";
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Reads a JSON body defensively. A gated route returns an explanatory
|
|
20
|
+
* `{success:false,error}` body with a non-2xx status; a genuine network/parse
|
|
21
|
+
* failure has no parseable body. Distinguishing the two is what stops a
|
|
22
|
+
* deliberate gate (403) from being disguised as "Network error" (BUG-5).
|
|
23
|
+
*/
|
|
24
|
+
async function readResult(
|
|
25
|
+
res: Response
|
|
26
|
+
): Promise<{ ok: boolean; body: Record<string, unknown> | null }> {
|
|
27
|
+
let body: Record<string, unknown> | null = null;
|
|
28
|
+
try {
|
|
29
|
+
body = (await res.json()) as Record<string, unknown> | null;
|
|
30
|
+
} catch {
|
|
31
|
+
body = null;
|
|
32
|
+
}
|
|
33
|
+
return { ok: res.ok, body };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Prefer the route's explanatory reason; fall back to the HTTP status. */
|
|
37
|
+
function failureMessage(
|
|
38
|
+
res: Response,
|
|
39
|
+
body: Record<string, unknown> | null,
|
|
40
|
+
fallback: string
|
|
41
|
+
): string {
|
|
42
|
+
if (body && typeof body.error === "string") return body.error;
|
|
43
|
+
return `${fallback} (HTTP ${res.status})`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function DataManagementSection({ allowed = true }: { allowed?: boolean }) {
|
|
19
47
|
const [clearOpen, setClearOpen] = useState(false);
|
|
20
48
|
const [seedOpen, setSeedOpen] = useState(false);
|
|
21
49
|
const [loading, setLoading] = useState(false);
|
|
@@ -24,14 +52,14 @@ export function DataManagementSection() {
|
|
|
24
52
|
setLoading(true);
|
|
25
53
|
try {
|
|
26
54
|
const res = await fetch("/api/data/clear", { method: "POST" });
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
-
const d =
|
|
55
|
+
const { ok, body } = await readResult(res);
|
|
56
|
+
if (ok && body?.success) {
|
|
57
|
+
const d = body.deleted as Record<string, number>;
|
|
30
58
|
toast.success(
|
|
31
59
|
`Cleared ${d.projects} projects, ${d.tasks} tasks, ${d.workflows} workflows, ${d.schedules} schedules, ${d.documents} documents, ${d.conversations} conversations, ${d.chatMessages} messages, ${d.learnedContext} learned context, ${d.views} views, ${d.usageLedger} usage entries, ${d.agentLogs} logs, ${d.notifications} notifications, ${d.sampleProfiles} sample profiles, ${d.files} files`
|
|
32
60
|
);
|
|
33
61
|
} else {
|
|
34
|
-
toast.error(
|
|
62
|
+
toast.error(failureMessage(res, body, "Clear failed"));
|
|
35
63
|
}
|
|
36
64
|
} catch {
|
|
37
65
|
toast.error("Clear failed. Network error");
|
|
@@ -44,14 +72,14 @@ export function DataManagementSection() {
|
|
|
44
72
|
setLoading(true);
|
|
45
73
|
try {
|
|
46
74
|
const res = await fetch("/api/data/seed", { method: "POST" });
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
const s =
|
|
75
|
+
const { ok, body } = await readResult(res);
|
|
76
|
+
if (ok && body?.success) {
|
|
77
|
+
const s = body.seeded as Record<string, number>;
|
|
50
78
|
toast.success(
|
|
51
79
|
`Seeded ${s.profiles} profiles, ${s.projects} projects, ${s.tasks} tasks, ${s.workflows} workflows, ${s.schedules} schedules, ${s.documents} documents, ${s.userTables} tables (${s.userTableRows} rows, ${s.tableViews} views, ${s.tableTriggers} triggers, ${s.tableRelationships} links), ${s.conversations} conversations, ${s.chatMessages} messages, ${s.agentMemory} memories, ${s.agentMessages} handoffs, ${s.channelConfigs} channels (${s.channelBindings} bindings), ${s.environmentScans} scans (${s.environmentArtifacts} artifacts, ${s.environmentCheckpoints} checkpoints, ${s.environmentTemplates} templates), ${s.workflowExecutionStats} workflow-stats, ${s.scheduleFiringMetrics} firing-metrics, ${s.usageLedger} usage entries, ${s.learnedContext} learned context, ${s.views} views, ${s.profileTestResults} test results, ${s.repoImports} repo imports, ${s.agentLogs} logs, ${s.notifications} notifications`
|
|
52
80
|
);
|
|
53
81
|
} else {
|
|
54
|
-
toast.error(
|
|
82
|
+
toast.error(failureMessage(res, body, "Seed failed"));
|
|
55
83
|
}
|
|
56
84
|
} catch {
|
|
57
85
|
toast.error("Seed failed. Network error");
|
|
@@ -60,6 +88,25 @@ export function DataManagementSection() {
|
|
|
60
88
|
}
|
|
61
89
|
}
|
|
62
90
|
|
|
91
|
+
if (!allowed) {
|
|
92
|
+
return (
|
|
93
|
+
<Card className="surface-card">
|
|
94
|
+
<CardHeader>
|
|
95
|
+
<CardTitle>Data Management</CardTitle>
|
|
96
|
+
<CardDescription>
|
|
97
|
+
Reset or populate your Orionfold Relay instance
|
|
98
|
+
</CardDescription>
|
|
99
|
+
</CardHeader>
|
|
100
|
+
<CardContent>
|
|
101
|
+
<p className="text-sm text-muted-foreground">
|
|
102
|
+
Seeding and clearing sample data are staging-only tools. They are
|
|
103
|
+
turned off on this build so your real data stays safe.
|
|
104
|
+
</p>
|
|
105
|
+
</CardContent>
|
|
106
|
+
</Card>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
63
110
|
return (
|
|
64
111
|
<>
|
|
65
112
|
<Card className="surface-card">
|
|
@@ -162,6 +162,7 @@ async function loadRuntimeStateUncached(
|
|
|
162
162
|
|
|
163
163
|
async function loadBaseline(app: AppDetail): Promise<RuntimeState> {
|
|
164
164
|
let recentTaskCount: number | undefined;
|
|
165
|
+
let activeRunCount = 0;
|
|
165
166
|
try {
|
|
166
167
|
const rows = db
|
|
167
168
|
.select({ value: count() })
|
|
@@ -169,12 +170,22 @@ async function loadBaseline(app: AppDetail): Promise<RuntimeState> {
|
|
|
169
170
|
.where(eq(tasks.projectId, app.id))
|
|
170
171
|
.all();
|
|
171
172
|
recentTaskCount = rows[0]?.value ?? 0;
|
|
173
|
+
// BUG-2: the header status must reflect whether the app is ACTUALLY
|
|
174
|
+
// running something, not a hardcoded literal. Count in-flight tasks
|
|
175
|
+
// (status = "running") for this app so kits can render Ready vs Running.
|
|
176
|
+
const running = db
|
|
177
|
+
.select({ value: count() })
|
|
178
|
+
.from(tasks)
|
|
179
|
+
.where(and(eq(tasks.projectId, app.id), eq(tasks.status, "running")))
|
|
180
|
+
.all();
|
|
181
|
+
activeRunCount = running[0]?.value ?? 0;
|
|
172
182
|
} catch {
|
|
173
183
|
recentTaskCount = undefined;
|
|
184
|
+
activeRunCount = 0;
|
|
174
185
|
}
|
|
175
186
|
const firstCron = app.manifest.schedules[0]?.cron;
|
|
176
187
|
const scheduleCadence = firstCron ? humanizeCron(firstCron) : null;
|
|
177
|
-
return { app, recentTaskCount, scheduleCadence };
|
|
188
|
+
return { app, recentTaskCount, scheduleCadence, activeRunCount };
|
|
178
189
|
}
|
|
179
190
|
|
|
180
191
|
async function loadCadence(
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { HeaderSlot, RuntimeState } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BUG-2: derive the app header's status chip from real run state instead of a
|
|
5
|
+
* hardcoded `"running"` literal. An app pulses "Running" ONLY while a task is
|
|
6
|
+
* actually in flight; otherwise it reads a calm, non-pulsing "Ready".
|
|
7
|
+
*
|
|
8
|
+
* All 7 view-kit builders share this so the fake-green-pulse-on-idle defect
|
|
9
|
+
* can't reappear in one kit. `activeRunCount` is populated once in the data
|
|
10
|
+
* layer's `loadBaseline` and spread into every kit's RuntimeState.
|
|
11
|
+
*/
|
|
12
|
+
export function headerStatus(runtime: RuntimeState): HeaderSlot["status"] {
|
|
13
|
+
return (runtime.activeRunCount ?? 0) > 0 ? "running" : "ready";
|
|
14
|
+
}
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
RuntimeState,
|
|
10
10
|
ViewModel,
|
|
11
11
|
} from "../types";
|
|
12
|
+
import { headerStatus } from "../header-status";
|
|
12
13
|
import type { BlueprintVariable } from "@/lib/workflows/blueprints/types";
|
|
13
14
|
|
|
14
15
|
interface CoachProjection extends KitProjection {
|
|
@@ -82,7 +83,7 @@ export const coachKit: KitDefinition = {
|
|
|
82
83
|
header: {
|
|
83
84
|
title: app.name,
|
|
84
85
|
description: app.description ?? undefined,
|
|
85
|
-
status:
|
|
86
|
+
status: headerStatus(runtime),
|
|
86
87
|
cadenceChip: runtime.cadence ?? undefined,
|
|
87
88
|
runNowBlueprintId: projection.runsBlueprintId,
|
|
88
89
|
runNowVariables: projection.runsBlueprintVars,
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
TriggerSource,
|
|
13
13
|
ViewModel,
|
|
14
14
|
} from "../types";
|
|
15
|
+
import { headerStatus } from "../header-status";
|
|
15
16
|
import type { BlueprintVariable } from "@/lib/workflows/blueprints/types";
|
|
16
17
|
|
|
17
18
|
interface InboxProjection extends KitProjection {
|
|
@@ -113,7 +114,7 @@ export const inboxKit: KitDefinition = {
|
|
|
113
114
|
header: {
|
|
114
115
|
title: app.name,
|
|
115
116
|
description: app.description ?? undefined,
|
|
116
|
-
status:
|
|
117
|
+
status: headerStatus(runtime),
|
|
117
118
|
runNowBlueprintId: isRowInsert ? undefined : projection.draftBlueprintId,
|
|
118
119
|
runNowVariables: projection.draftBlueprintVars,
|
|
119
120
|
triggerSourceChip: projection.triggerSource,
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
RuntimeState,
|
|
14
14
|
ViewModel,
|
|
15
15
|
} from "../types";
|
|
16
|
+
import { headerStatus } from "../header-status";
|
|
16
17
|
import type { BlueprintVariable } from "@/lib/workflows/blueprints/types";
|
|
17
18
|
|
|
18
19
|
type KpiSpec = NonNullable<ViewConfig["bindings"]["kpis"]>[number];
|
|
@@ -134,7 +135,7 @@ export const ledgerKit: KitDefinition = {
|
|
|
134
135
|
header: {
|
|
135
136
|
title: app.name,
|
|
136
137
|
description: app.description ?? undefined,
|
|
137
|
-
status:
|
|
138
|
+
status: headerStatus(runtime),
|
|
138
139
|
runNowBlueprintId: projection.runsBlueprintId,
|
|
139
140
|
runNowVariables: projection.runsBlueprintVars,
|
|
140
141
|
periodChip: { current: projection.period },
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
RuntimeState,
|
|
9
9
|
ViewModel,
|
|
10
10
|
} from "../types";
|
|
11
|
+
import { headerStatus } from "../header-status";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Phase 1.1 placeholder kit. Lands the seam: every app dispatches through
|
|
@@ -41,7 +42,7 @@ export const placeholderKit: KitDefinition = {
|
|
|
41
42
|
header: {
|
|
42
43
|
title: app.name,
|
|
43
44
|
description: app.description ?? "Composed app",
|
|
44
|
-
status:
|
|
45
|
+
status: headerStatus(runtime),
|
|
45
46
|
},
|
|
46
47
|
footer: {
|
|
47
48
|
appId: app.id,
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
RuntimeState,
|
|
12
12
|
ViewModel,
|
|
13
13
|
} from "../types";
|
|
14
|
+
import { headerStatus } from "../header-status";
|
|
14
15
|
import type { BlueprintVariable } from "@/lib/workflows/blueprints/types";
|
|
15
16
|
|
|
16
17
|
interface ResearchProjection extends KitProjection {
|
|
@@ -106,7 +107,7 @@ export const researchKit: KitDefinition = {
|
|
|
106
107
|
header: {
|
|
107
108
|
title: app.name,
|
|
108
109
|
description: app.description ?? undefined,
|
|
109
|
-
status:
|
|
110
|
+
status: headerStatus(runtime),
|
|
110
111
|
cadenceChip: runtime.cadence ?? undefined,
|
|
111
112
|
runNowBlueprintId: projection.synthesisBlueprintId,
|
|
112
113
|
runNowVariables: projection.synthesisBlueprintVars,
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
RuntimeState,
|
|
12
12
|
ViewModel,
|
|
13
13
|
} from "../types";
|
|
14
|
+
import { headerStatus } from "../header-status";
|
|
14
15
|
|
|
15
16
|
type KpiSpec = NonNullable<ViewConfig["bindings"]["kpis"]>[number];
|
|
16
17
|
|
|
@@ -103,7 +104,7 @@ export const trackerKit: KitDefinition = {
|
|
|
103
104
|
header: {
|
|
104
105
|
title: app.name,
|
|
105
106
|
description: app.description ?? undefined,
|
|
106
|
-
status:
|
|
107
|
+
status: headerStatus(runtime),
|
|
107
108
|
cadenceChip: runtime.cadence ?? undefined,
|
|
108
109
|
runNowBlueprintId: projection.runsBlueprintId,
|
|
109
110
|
},
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
RuntimeState,
|
|
12
12
|
ViewModel,
|
|
13
13
|
} from "../types";
|
|
14
|
+
import { headerStatus } from "../header-status";
|
|
14
15
|
|
|
15
16
|
type KpiSpec = NonNullable<ViewConfig["bindings"]["kpis"]>[number];
|
|
16
17
|
|
|
@@ -120,7 +121,7 @@ export const workflowHubKit: KitDefinition = {
|
|
|
120
121
|
header: {
|
|
121
122
|
title: app.name,
|
|
122
123
|
description: app.description ?? "Composed app",
|
|
123
|
-
status:
|
|
124
|
+
status: headerStatus(runtime),
|
|
124
125
|
cadenceChip: runtime.cadence ?? undefined,
|
|
125
126
|
},
|
|
126
127
|
kpis: runtime.evaluatedKpis ?? [],
|
|
@@ -65,6 +65,12 @@ export interface TimelineRun {
|
|
|
65
65
|
export interface RuntimeState {
|
|
66
66
|
app: AppDetail;
|
|
67
67
|
recentTaskCount?: number;
|
|
68
|
+
/**
|
|
69
|
+
* BUG-2: number of tasks currently in flight (status = "running") for this
|
|
70
|
+
* app. Drives the header status chip — Ready (idle) vs Running — so an idle
|
|
71
|
+
* app never pulses a fake "Running" literal.
|
|
72
|
+
*/
|
|
73
|
+
activeRunCount?: number;
|
|
68
74
|
scheduleCadence?: string | null;
|
|
69
75
|
/** Phase 2: hero table content for Tracker kit (columns + last-N rows). */
|
|
70
76
|
heroTable?: HeroTableData | null;
|
|
@@ -176,7 +182,7 @@ export type TriggerSource =
|
|
|
176
182
|
export interface HeaderSlot {
|
|
177
183
|
title: string;
|
|
178
184
|
description?: string;
|
|
179
|
-
status?: "running" | "queued" | "completed" | "failed" | "planned";
|
|
185
|
+
status?: "ready" | "running" | "queued" | "completed" | "failed" | "planned";
|
|
180
186
|
/** Right-aligned actions; rendered as ReactNode so kits can compose. */
|
|
181
187
|
actions?: ReactNode;
|
|
182
188
|
/** Phase 2: render a ScheduleCadenceChip when present. */
|
|
@@ -41,6 +41,11 @@ export function searchFiles(
|
|
|
41
41
|
encoding: "utf-8",
|
|
42
42
|
maxBuffer: 10 * 1024 * 1024,
|
|
43
43
|
timeout: 3000,
|
|
44
|
+
// Route git's stderr to a pipe (discarded via the catch) instead of
|
|
45
|
+
// inheriting the console, so a non-git cwd can't leak a raw
|
|
46
|
+
// `fatal: not a git repository` line to a customer's console. Mirrors
|
|
47
|
+
// src/lib/environment/workspace-context.ts / src/lib/instance/git-ops.ts.
|
|
48
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
44
49
|
}
|
|
45
50
|
);
|
|
46
51
|
} catch {
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
createImportRecord,
|
|
27
27
|
} from "@/lib/tables/import";
|
|
28
28
|
import { createEnrichmentWorkflow } from "@/lib/tables/enrichment";
|
|
29
|
+
import { getSelfBaseUrl } from "@/lib/http/self-base-url";
|
|
29
30
|
import type { ColumnDef } from "@/lib/tables/types";
|
|
30
31
|
|
|
31
32
|
export function tableTools(ctx: ToolContext) {
|
|
@@ -599,7 +600,7 @@ Guidelines for schema inference:
|
|
|
599
600
|
try {
|
|
600
601
|
const table = await getTable(args.tableId);
|
|
601
602
|
if (!table) return err("Table not found");
|
|
602
|
-
const baseUrl =
|
|
603
|
+
const baseUrl = getSelfBaseUrl();
|
|
603
604
|
return ok({
|
|
604
605
|
url: `${baseUrl}/api/tables/${args.tableId}/export?format=${args.format}`,
|
|
605
606
|
table: table.name,
|
|
@@ -1067,5 +1068,7 @@ Guidelines for schema inference:
|
|
|
1067
1068
|
}
|
|
1068
1069
|
|
|
1069
1070
|
function getBaseUrl(): string {
|
|
1070
|
-
|
|
1071
|
+
// Loopback self-call origin — derived from the real bind port, not a bare
|
|
1072
|
+
// :3000 literal (issue #29). See @/lib/http/self-base-url.
|
|
1073
|
+
return getSelfBaseUrl();
|
|
1071
1074
|
}
|
|
@@ -41,12 +41,24 @@ export interface StatusDefinition {
|
|
|
41
41
|
live?: boolean;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// Neutral, non-pulsing surface token (used by planned/ready idle states).
|
|
45
|
+
const MUTED = "muted-foreground";
|
|
46
|
+
|
|
44
47
|
// ── Lifecycle statuses ───────────────────────────────────────────
|
|
45
48
|
export const lifecycleStatuses: Record<string, StatusDefinition> = {
|
|
46
49
|
planned: {
|
|
47
50
|
label: "Planned",
|
|
48
51
|
icon: Circle,
|
|
49
|
-
colorToken:
|
|
52
|
+
colorToken: MUTED,
|
|
53
|
+
badgeVariant: "outline",
|
|
54
|
+
},
|
|
55
|
+
// BUG-2: installed & healthy with nothing in flight. Calm, NON-pulsing —
|
|
56
|
+
// an idle app must not show the green "Running" ping. Neutral (muted) reads
|
|
57
|
+
// as "steady/ready", not a success/completion state.
|
|
58
|
+
ready: {
|
|
59
|
+
label: "Ready",
|
|
60
|
+
icon: CheckCircle2,
|
|
61
|
+
colorToken: MUTED,
|
|
50
62
|
badgeVariant: "outline",
|
|
51
63
|
},
|
|
52
64
|
queued: {
|
|
@@ -19,6 +19,11 @@ function git(args: string[], cwd: string): GitResult {
|
|
|
19
19
|
cwd,
|
|
20
20
|
encoding: "utf-8",
|
|
21
21
|
timeout: 10000,
|
|
22
|
+
// Route git's stderr to a pipe (discarded here) instead of inheriting
|
|
23
|
+
// the console, so a non-git cwd can't leak a raw `fatal: not a git
|
|
24
|
+
// repository` line to a customer's first-run log. Mirrors
|
|
25
|
+
// src/lib/environment/workspace-context.ts / src/lib/instance/git-ops.ts.
|
|
26
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
22
27
|
}).trim();
|
|
23
28
|
return { success: true, output };
|
|
24
29
|
} catch (e) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base URL for Relay's internal loopback self-calls (server → its OWN API).
|
|
3
|
+
*
|
|
4
|
+
* Several server-side paths call Relay's own HTTP API — the table-trigger
|
|
5
|
+
* dispatcher fires `POST /api/tasks` / `/api/workflows/:id/execute`, and the
|
|
6
|
+
* compose chat tools hit `/api/tables/:id/triggers`, `/export`, `/templates`,
|
|
7
|
+
* etc. These are loopback self-fetches, so the origin must be *this* server's
|
|
8
|
+
* own address, not a hardcoded one.
|
|
9
|
+
*
|
|
10
|
+
* The bug this fixes: the old fallback was a bare `http://localhost:3000`, which
|
|
11
|
+
* silently broke every instance NOT on port 3000 (`--port`, `--hostname
|
|
12
|
+
* 0.0.0.0`, containers, staging on :3199). The CLI sets neither `NEXTAUTH_URL`
|
|
13
|
+
* nor `NEXT_PUBLIC_APP_URL`, so that fallback was always used → `TypeError:
|
|
14
|
+
* fetch failed`, caught + swallowed to a server-log line, zero user-facing
|
|
15
|
+
* signal (issue #29). See memory `self-http-calls-hardcode-3000`.
|
|
16
|
+
*
|
|
17
|
+
* Precedence:
|
|
18
|
+
* 1. `RELAY_SELF_BASE_URL` — explicit override the CLI threads from the known
|
|
19
|
+
* bind port (loopback host + real port).
|
|
20
|
+
* 2. `NEXTAUTH_URL` / `NEXT_PUBLIC_APP_URL` — reverse-proxy / custom-origin
|
|
21
|
+
* escape hatch (unchanged behavior for those deployments).
|
|
22
|
+
* 3. `http://127.0.0.1:${PORT}` — loopback + the real port. The server always
|
|
23
|
+
* listens on loopback even when bound to `0.0.0.0` (INADDR_ANY includes the
|
|
24
|
+
* loopback interface), so a self-call must target 127.0.0.1, never the LAN
|
|
25
|
+
* IP or `0.0.0.0`. `PORT` is already in the child env (`bin/cli.ts`).
|
|
26
|
+
* 4. `http://127.0.0.1:3000` — last-resort default when even `PORT` is absent.
|
|
27
|
+
*
|
|
28
|
+
* ZERO-IMPORT LEAF: this module reads only `process.env` and does string work.
|
|
29
|
+
* It is imported by `table-tools.ts`, which is reachable from the runtime
|
|
30
|
+
* catalog — a non-leaf import here would risk the module-load cycle the
|
|
31
|
+
* smoke-budget rule guards. Keep it import-free. See memory
|
|
32
|
+
* `shared-constant-zero-import-leaf`.
|
|
33
|
+
*/
|
|
34
|
+
export function getSelfBaseUrl(): string {
|
|
35
|
+
const explicit =
|
|
36
|
+
process.env.RELAY_SELF_BASE_URL ||
|
|
37
|
+
process.env.NEXTAUTH_URL ||
|
|
38
|
+
process.env.NEXT_PUBLIC_APP_URL;
|
|
39
|
+
if (explicit) return explicit;
|
|
40
|
+
const port = process.env.PORT || "3000";
|
|
41
|
+
return `http://127.0.0.1:${port}`;
|
|
42
|
+
}
|
package/src/lib/packs/install.ts
CHANGED
|
@@ -361,6 +361,25 @@ export async function installPack(
|
|
|
361
361
|
reloadBlueprints();
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
// Defense-in-depth for the app-detail Data Cache. `loadRuntimeState`
|
|
365
|
+
// (src/lib/apps/view-kits/data.ts) wraps its projection in
|
|
366
|
+
// unstable_cache({ revalidate:30, tags:['app-runtime:<id>'] }). Reloading
|
|
367
|
+
// the registry above fixes getBlueprint(), but a Data-Cache snapshot taken
|
|
368
|
+
// in the 30s window before enrichment populated could still serve husk
|
|
369
|
+
// cards (raw ids, no Run button). Invalidating the tag here guarantees the
|
|
370
|
+
// freshly-installed app's page reads a live projection, not a stale one.
|
|
371
|
+
// (Did not reproduce on a from-scratch install — the registry singleton is
|
|
372
|
+
// populated before first render — but this closes the latent race.)
|
|
373
|
+
try {
|
|
374
|
+
const { revalidateTag } = await import("next/cache");
|
|
375
|
+
// Next 16 requires a profile/expiry arg. expire:0 = purge the tagged
|
|
376
|
+
// entry immediately so the next render recomputes the projection.
|
|
377
|
+
revalidateTag(`app-runtime:${pack.meta.id}`, { expire: 0 });
|
|
378
|
+
} catch {
|
|
379
|
+
// revalidateTag is a no-op outside a Next request/render scope (e.g. the
|
|
380
|
+
// CLI install path). Never let cache housekeeping fail an install.
|
|
381
|
+
}
|
|
382
|
+
|
|
364
383
|
// 6. Report — zero silent steps.
|
|
365
384
|
return {
|
|
366
385
|
packId: pack.meta.id,
|
|
@@ -53,7 +53,7 @@ import type { ScheduleSpec } from "@/lib/validators/schedule-spec";
|
|
|
53
53
|
// unfixed from 0.15.0 through 0.16.0 — treat the window test's failure as
|
|
54
54
|
// a release blocker, not noise). The 0.13→0.14 three-MINOR bridge is over;
|
|
55
55
|
// this is the standard 2-MINOR window now.
|
|
56
|
-
const SUPPORTED_API_VERSIONS = new Set([CURRENT_PLUGIN_API_VERSION, "0.
|
|
56
|
+
const SUPPORTED_API_VERSIONS = new Set([CURRENT_PLUGIN_API_VERSION, "0.25"]);
|
|
57
57
|
|
|
58
58
|
/** Test-helper export so the window-enforcement test can read state. */
|
|
59
59
|
export function isSupportedApiVersion(apiVersion: string): boolean {
|
|
@@ -6,7 +6,7 @@ import { z } from "zod";
|
|
|
6
6
|
// (a hardcoded copy there once drifted to "0.14" — scaffolded plugins would
|
|
7
7
|
// have been disabled on load the moment the window tightened). Bump on every
|
|
8
8
|
// MINOR release; api-version-window.test.ts fails if this goes stale.
|
|
9
|
-
export const CURRENT_PLUGIN_API_VERSION = "0.
|
|
9
|
+
export const CURRENT_PLUGIN_API_VERSION = "0.26";
|
|
10
10
|
|
|
11
11
|
// Shared capability tuple — single source of truth used by Zod schema and
|
|
12
12
|
// capability-check.ts hash derivation. Exported so consumers don't need a
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { db } from "@/lib/db";
|
|
7
7
|
import { userTableTriggers } from "@/lib/db/schema";
|
|
8
8
|
import { eq, and } from "drizzle-orm";
|
|
9
|
+
import { getSelfBaseUrl } from "@/lib/http/self-base-url";
|
|
9
10
|
import type { FilterSpec } from "./types";
|
|
10
11
|
|
|
11
12
|
type TriggerEvent = "row_added" | "row_updated" | "row_deleted";
|
|
@@ -147,13 +148,15 @@ async function fireAction(
|
|
|
147
148
|
? `${config.description}\n\nTrigger data: ${JSON.stringify(rowData, null, 2)}`
|
|
148
149
|
: `Triggered by table row change.\n\nData: ${JSON.stringify(rowData, null, 2)}`;
|
|
149
150
|
|
|
150
|
-
await fetch(`${
|
|
151
|
+
await fetch(`${getSelfBaseUrl()}/api/tasks`, {
|
|
151
152
|
method: "POST",
|
|
152
153
|
headers: { "Content-Type": "application/json" },
|
|
153
154
|
body: JSON.stringify({
|
|
154
155
|
title: config.title ?? "Triggered Task",
|
|
155
156
|
description,
|
|
156
|
-
|
|
157
|
+
// createTaskSchema wants `string | undefined`, not null — sending null
|
|
158
|
+
// 400s the self-call and the task is never created. Omit when absent.
|
|
159
|
+
...(config.projectId ? { projectId: config.projectId } : {}),
|
|
157
160
|
}),
|
|
158
161
|
});
|
|
159
162
|
return;
|
|
@@ -187,7 +190,7 @@ async function fireAction(
|
|
|
187
190
|
}
|
|
188
191
|
|
|
189
192
|
if (actionType === "run_workflow" && config.workflowId) {
|
|
190
|
-
await fetch(`${
|
|
193
|
+
await fetch(`${getSelfBaseUrl()}/api/workflows/${config.workflowId}/execute`, {
|
|
191
194
|
method: "POST",
|
|
192
195
|
headers: { "Content-Type": "application/json" },
|
|
193
196
|
body: JSON.stringify({
|
|
@@ -212,6 +215,3 @@ function deriveAppIdFromBlueprintId(blueprintId: string): string | null {
|
|
|
212
215
|
return blueprintId.slice(0, idx);
|
|
213
216
|
}
|
|
214
217
|
|
|
215
|
-
function getBaseUrl(): string {
|
|
216
|
-
return process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
|
|
217
|
-
}
|