@tradejs/app 3.1.8-beta.207 → 3.1.8-beta.212
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/package.json +6 -6
- package/src/app/api/user/runtime-deployments/[deploymentId]/strategies/[strategyName]/control/route.ts +37 -14
- package/src/app/api/user/runtime-deployments/route.ts +6 -119
- package/src/app/api/user/trading-accounts/[accountId]/route.ts +6 -4
- package/src/app/components/Strategies/RuntimeStrategyCard.tsx +1 -2
- package/src/app/components/Strategies/RuntimeStrategyChart.tsx +2 -57
- package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +3 -3
- package/src/app/api/user/runtime-deployments/[deploymentId]/route.ts +0 -25
- package/src/app/components/Strategies/StrategyEvidencePopover.tsx +0 -261
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tradejs/app",
|
|
3
|
-
"version": "3.1.8-beta.
|
|
3
|
+
"version": "3.1.8-beta.212",
|
|
4
4
|
"description": "Installable Next.js UI for the TradeJS TypeScript framework: dashboards, backtests, charts, and runtime data.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tradejs",
|
|
@@ -52,11 +52,11 @@
|
|
|
52
52
|
"@chakra-ui/charts": "3.36.1",
|
|
53
53
|
"@chakra-ui/react": "3.36.1",
|
|
54
54
|
"@emotion/react": "^11.14.0",
|
|
55
|
-
"@tradejs/core": "^3.1.8-beta.
|
|
56
|
-
"@tradejs/indicators": "^3.1.8-beta.
|
|
57
|
-
"@tradejs/infra": "^3.1.8-beta.
|
|
58
|
-
"@tradejs/node": "^3.1.8-beta.
|
|
59
|
-
"@tradejs/types": "^3.1.8-beta.
|
|
55
|
+
"@tradejs/core": "^3.1.8-beta.212",
|
|
56
|
+
"@tradejs/indicators": "^3.1.8-beta.212",
|
|
57
|
+
"@tradejs/infra": "^3.1.8-beta.212",
|
|
58
|
+
"@tradejs/node": "^3.1.8-beta.212",
|
|
59
|
+
"@tradejs/types": "^3.1.8-beta.212",
|
|
60
60
|
"@types/bcryptjs": "2.4.6",
|
|
61
61
|
"@types/lodash": "4.17.24",
|
|
62
62
|
"@types/node": "24.13.3",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
pauseRuntimeStrategy,
|
|
4
|
+
recordRuntimeStrategyControlEvent,
|
|
5
|
+
resumeRuntimeStrategy,
|
|
6
|
+
} from '@tradejs/infra/runtimeControls';
|
|
7
|
+
import { getRuntimeDeployment } from '@tradejs/node/runtimeStrategies';
|
|
7
8
|
import type { RuntimeStrategyControlState } from '@tradejs/types';
|
|
8
9
|
import { getCurrentUserName } from '#app/lib/currentUser';
|
|
9
10
|
|
|
@@ -21,7 +22,13 @@ export const PATCH = async (
|
|
|
21
22
|
}
|
|
22
23
|
const { deploymentId, strategyName: encodedStrategyName } = await params;
|
|
23
24
|
const strategyName = decodeURIComponent(encodedStrategyName);
|
|
24
|
-
const
|
|
25
|
+
const projectRoot =
|
|
26
|
+
String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
|
|
27
|
+
const deployment = await getRuntimeDeployment({
|
|
28
|
+
userName,
|
|
29
|
+
projectRoot,
|
|
30
|
+
deploymentId,
|
|
31
|
+
});
|
|
25
32
|
if (!deployment) {
|
|
26
33
|
return NextResponse.json(
|
|
27
34
|
{ error: 'Deployment not found' },
|
|
@@ -44,7 +51,7 @@ export const PATCH = async (
|
|
|
44
51
|
);
|
|
45
52
|
if (!reference) {
|
|
46
53
|
return NextResponse.json(
|
|
47
|
-
{ error: '
|
|
54
|
+
{ error: 'Strategy is not declared in tradejs.config.ts' },
|
|
48
55
|
{ status: 400 },
|
|
49
56
|
);
|
|
50
57
|
}
|
|
@@ -52,19 +59,35 @@ export const PATCH = async (
|
|
|
52
59
|
if (previousState === controlState) {
|
|
53
60
|
return NextResponse.json({ deployment, controlState });
|
|
54
61
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
if (
|
|
63
|
+
controlState === 'active' &&
|
|
64
|
+
(!deployment.enabled || !reference.enabled)
|
|
65
|
+
) {
|
|
66
|
+
return NextResponse.json(
|
|
67
|
+
{ error: 'Strategy is disabled in tradejs.config.ts' },
|
|
68
|
+
{ status: 409 },
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (controlState === 'entries_paused') {
|
|
72
|
+
await pauseRuntimeStrategy({
|
|
73
|
+
userName,
|
|
74
|
+
deploymentId,
|
|
75
|
+
strategyName,
|
|
76
|
+
updatedBy: userName,
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
await resumeRuntimeStrategy({ userName, deploymentId, strategyName });
|
|
80
|
+
}
|
|
81
|
+
const updated = await getRuntimeDeployment({
|
|
82
|
+
userName,
|
|
83
|
+
projectRoot,
|
|
84
|
+
deploymentId,
|
|
62
85
|
});
|
|
63
86
|
const event = await recordRuntimeStrategyControlEvent({
|
|
64
87
|
userName,
|
|
65
88
|
deploymentId: deployment.id,
|
|
66
89
|
strategyName,
|
|
67
|
-
|
|
90
|
+
version: reference.version,
|
|
68
91
|
previousState,
|
|
69
92
|
nextState: controlState,
|
|
70
93
|
createdBy: userName,
|
|
@@ -1,44 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
listRuntimeDeployments,
|
|
5
|
-
saveRuntimeDeployment,
|
|
6
|
-
} from '@tradejs/infra/runtimeDeployments';
|
|
7
|
-
import { getTradingAccount } from '@tradejs/infra/tradingAccounts';
|
|
8
|
-
import { getRuntimeStrategyRelease } from '@tradejs/infra/runtimeStrategyReleases';
|
|
9
|
-
import type {
|
|
10
|
-
MarketUniverse,
|
|
11
|
-
RuntimeDeployment,
|
|
12
|
-
RuntimeDeploymentStrategy,
|
|
13
|
-
} from '@tradejs/types';
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { getRuntimeDeploymentHeartbeat } from '@tradejs/infra/runtimeHeartbeats';
|
|
3
|
+
import { listRuntimeDeployments } from '@tradejs/node/runtimeStrategies';
|
|
14
4
|
import { getCurrentUserName } from '#app/lib/currentUser';
|
|
15
5
|
|
|
16
6
|
export const dynamic = 'force-dynamic';
|
|
17
7
|
|
|
18
|
-
const isRuntimeStrategyReference = (
|
|
19
|
-
value: unknown,
|
|
20
|
-
): value is RuntimeDeploymentStrategy => {
|
|
21
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
22
|
-
const record = value as Record<string, unknown>;
|
|
23
|
-
return (
|
|
24
|
-
typeof record.strategyName === 'string' &&
|
|
25
|
-
record.strategyName.trim().length > 0 &&
|
|
26
|
-
Number.isSafeInteger(record.releaseVersion) &&
|
|
27
|
-
Number(record.releaseVersion) > 0 &&
|
|
28
|
-
(record.controlState === 'active' ||
|
|
29
|
-
record.controlState === 'entries_paused') &&
|
|
30
|
-
Object.keys(record).every((key) =>
|
|
31
|
-
['strategyName', 'releaseVersion', 'controlState'].includes(key),
|
|
32
|
-
)
|
|
33
|
-
);
|
|
34
|
-
};
|
|
35
|
-
|
|
36
8
|
export const GET = async () => {
|
|
37
9
|
const userName = await getCurrentUserName();
|
|
38
10
|
if (!userName) {
|
|
39
11
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
40
12
|
}
|
|
41
|
-
const
|
|
13
|
+
const projectRoot =
|
|
14
|
+
String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
|
|
15
|
+
const deployments = await listRuntimeDeployments({ userName, projectRoot });
|
|
42
16
|
const withHeartbeats = await Promise.all(
|
|
43
17
|
deployments.map(async (deployment) => ({
|
|
44
18
|
...deployment,
|
|
@@ -47,90 +21,3 @@ export const GET = async () => {
|
|
|
47
21
|
);
|
|
48
22
|
return NextResponse.json({ deployments: withHeartbeats });
|
|
49
23
|
};
|
|
50
|
-
|
|
51
|
-
export const POST = async (request: NextRequest) => {
|
|
52
|
-
const userName = await getCurrentUserName();
|
|
53
|
-
if (!userName) {
|
|
54
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
55
|
-
}
|
|
56
|
-
try {
|
|
57
|
-
const body = (await request.json()) as Partial<RuntimeDeployment>;
|
|
58
|
-
if (
|
|
59
|
-
!Array.isArray(body.strategies) ||
|
|
60
|
-
!body.strategies.length ||
|
|
61
|
-
!body.strategies.every(isRuntimeStrategyReference)
|
|
62
|
-
) {
|
|
63
|
-
return NextResponse.json(
|
|
64
|
-
{ error: 'Invalid runtime strategy reference' },
|
|
65
|
-
{ status: 400 },
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
const releases = await Promise.all(
|
|
69
|
-
body.strategies.map(async (strategy) => {
|
|
70
|
-
const release = await getRuntimeStrategyRelease(
|
|
71
|
-
userName,
|
|
72
|
-
strategy.strategyName,
|
|
73
|
-
strategy.releaseVersion,
|
|
74
|
-
);
|
|
75
|
-
if (!release) {
|
|
76
|
-
throw new Error(
|
|
77
|
-
`Release not found: ${strategy.strategyName} v${strategy.releaseVersion}`,
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
return release;
|
|
81
|
-
}),
|
|
82
|
-
);
|
|
83
|
-
if (
|
|
84
|
-
!body.id ||
|
|
85
|
-
!body.label ||
|
|
86
|
-
!body.connectorName ||
|
|
87
|
-
!body.provider ||
|
|
88
|
-
!body.accountId ||
|
|
89
|
-
typeof body.enabled !== 'boolean'
|
|
90
|
-
) {
|
|
91
|
-
return NextResponse.json(
|
|
92
|
-
{ error: 'Invalid runtime deployment' },
|
|
93
|
-
{ status: 400 },
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
const account = await getTradingAccount(userName, body.accountId);
|
|
97
|
-
if (
|
|
98
|
-
!account ||
|
|
99
|
-
!account.enabled ||
|
|
100
|
-
account.provider !== body.provider.toLowerCase()
|
|
101
|
-
) {
|
|
102
|
-
return NextResponse.json(
|
|
103
|
-
{ error: 'Deployment trading account is unavailable' },
|
|
104
|
-
{ status: 400 },
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
const unsupportedUniverse = releases
|
|
108
|
-
.map((release) => release.config.UNIVERSE as MarketUniverse)
|
|
109
|
-
.find((universe) => !account.universes.includes(universe));
|
|
110
|
-
if (unsupportedUniverse) {
|
|
111
|
-
return NextResponse.json(
|
|
112
|
-
{
|
|
113
|
-
error: `Account ${account.id} does not support ${unsupportedUniverse}`,
|
|
114
|
-
},
|
|
115
|
-
{ status: 400 },
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
const deployment = await saveRuntimeDeployment(userName, {
|
|
119
|
-
id: body.id,
|
|
120
|
-
label: body.label,
|
|
121
|
-
connectorName: body.connectorName,
|
|
122
|
-
provider: body.provider,
|
|
123
|
-
accountId: body.accountId,
|
|
124
|
-
enabled: body.enabled,
|
|
125
|
-
strategies: body.strategies,
|
|
126
|
-
assetClasses: body.assetClasses,
|
|
127
|
-
tickers: body.tickers,
|
|
128
|
-
});
|
|
129
|
-
return NextResponse.json({ deployment });
|
|
130
|
-
} catch (error) {
|
|
131
|
-
return NextResponse.json(
|
|
132
|
-
{ error: error instanceof Error ? error.message : String(error) },
|
|
133
|
-
{ status: 400 },
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
};
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
deleteTradingAccount,
|
|
4
4
|
getTradingAccount,
|
|
5
5
|
} from '@tradejs/infra/tradingAccounts';
|
|
6
|
-
import { listRuntimeDeployments } from '@tradejs/
|
|
6
|
+
import { listRuntimeDeployments } from '@tradejs/node/runtimeStrategies';
|
|
7
7
|
import { getCurrentUserName } from '#app/lib/currentUser';
|
|
8
8
|
|
|
9
9
|
export const DELETE = async (
|
|
@@ -19,9 +19,11 @@ export const DELETE = async (
|
|
|
19
19
|
if (!account) {
|
|
20
20
|
return NextResponse.json({ error: 'Account not found' }, { status: 404 });
|
|
21
21
|
}
|
|
22
|
-
const
|
|
23
|
-
(
|
|
24
|
-
|
|
22
|
+
const projectRoot =
|
|
23
|
+
String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
|
|
24
|
+
const referencedBy = (
|
|
25
|
+
await listRuntimeDeployments({ userName, projectRoot })
|
|
26
|
+
).filter((deployment) => deployment.accountId === account.id);
|
|
25
27
|
if (referencedBy.length) {
|
|
26
28
|
return NextResponse.json(
|
|
27
29
|
{
|
|
@@ -135,7 +135,7 @@ export const RuntimeStrategyCard = ({
|
|
|
135
135
|
TF: {strategy.interval}m
|
|
136
136
|
</Badge>
|
|
137
137
|
<Badge colorPalette="gray" variant="outline">
|
|
138
|
-
|
|
138
|
+
version: v{strategy.version}
|
|
139
139
|
</Badge>
|
|
140
140
|
{strategy.accountId ? (
|
|
141
141
|
<Badge colorPalette="purple" variant="outline">
|
|
@@ -258,7 +258,6 @@ export const RuntimeStrategyCard = ({
|
|
|
258
258
|
<RuntimeStrategyChart
|
|
259
259
|
orderLog={strategy.orderLog}
|
|
260
260
|
stat={strategy.stat}
|
|
261
|
-
evidenceTimeline={strategy.evidenceTimeline}
|
|
262
261
|
startTimestamp={startTimestamp}
|
|
263
262
|
endTimestamp={endTimestamp}
|
|
264
263
|
/>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useMemo
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
4
|
import { Box, Flex, Text } from '@chakra-ui/react';
|
|
5
5
|
import { Chart, useChart } from '@chakra-ui/charts';
|
|
6
6
|
import {
|
|
@@ -13,24 +13,13 @@ import {
|
|
|
13
13
|
YAxis,
|
|
14
14
|
} from 'recharts';
|
|
15
15
|
import { getFormatted } from '@tradejs/core/backtest';
|
|
16
|
-
import type {
|
|
17
|
-
SimpleOrderLogData,
|
|
18
|
-
StrategyEvidenceMarkerType,
|
|
19
|
-
StrategyEvidenceTimeline,
|
|
20
|
-
TestStat,
|
|
21
|
-
} from '@tradejs/types';
|
|
16
|
+
import type { SimpleOrderLogData, TestStat } from '@tradejs/types';
|
|
22
17
|
import { formatTimeSeriesTooltipTimestamp } from '#app/lib/timeSeriesChart';
|
|
23
18
|
import { TimeSeriesXAxis } from '#shared/Charts/TimeSeriesXAxis';
|
|
24
|
-
import {
|
|
25
|
-
filterStrategyEvidenceMarkers,
|
|
26
|
-
STRATEGY_EVIDENCE_MARKER_PRESENTATION,
|
|
27
|
-
StrategyEvidencePopover,
|
|
28
|
-
} from './StrategyEvidencePopover';
|
|
29
19
|
|
|
30
20
|
interface RuntimeStrategyChartProps {
|
|
31
21
|
orderLog: SimpleOrderLogData;
|
|
32
22
|
stat: TestStat;
|
|
33
|
-
evidenceTimeline: StrategyEvidenceTimeline;
|
|
34
23
|
startTimestamp: number;
|
|
35
24
|
endTimestamp: number;
|
|
36
25
|
height?: string | number;
|
|
@@ -39,13 +28,10 @@ interface RuntimeStrategyChartProps {
|
|
|
39
28
|
export const RuntimeStrategyChart = ({
|
|
40
29
|
orderLog,
|
|
41
30
|
stat,
|
|
42
|
-
evidenceTimeline,
|
|
43
31
|
startTimestamp,
|
|
44
32
|
endTimestamp,
|
|
45
33
|
height = '350px',
|
|
46
34
|
}: RuntimeStrategyChartProps) => {
|
|
47
|
-
const [showParity, setShowParity] = useState(true);
|
|
48
|
-
const [showRecommendations, setShowRecommendations] = useState(true);
|
|
49
35
|
const chartData = useMemo(
|
|
50
36
|
() => ({
|
|
51
37
|
data: orderLog.map(([timestamp, amount]) => ({
|
|
@@ -65,31 +51,8 @@ export const RuntimeStrategyChart = ({
|
|
|
65
51
|
const chart = useChart(chartData as any);
|
|
66
52
|
const { formatted: maxAmount } = getFormatted(stat, 'maxAmount');
|
|
67
53
|
const { formatted: minAmount } = getFormatted(stat, 'minAmount');
|
|
68
|
-
const visibleEvidenceMarkers = filterStrategyEvidenceMarkers({
|
|
69
|
-
markers:
|
|
70
|
-
evidenceTimeline.status === 'verified' ? evidenceTimeline.markers : [],
|
|
71
|
-
showParity,
|
|
72
|
-
showRecommendations,
|
|
73
|
-
});
|
|
74
|
-
const markerColor = (type: StrategyEvidenceMarkerType) =>
|
|
75
|
-
chart.color(STRATEGY_EVIDENCE_MARKER_PRESENTATION[type].color);
|
|
76
|
-
const markerDash = (type: StrategyEvidenceMarkerType) => {
|
|
77
|
-
if (type === 'L') return '7 4';
|
|
78
|
-
if (type === 'P' || type === 'R') return '2 5';
|
|
79
|
-
return '3 5';
|
|
80
|
-
};
|
|
81
|
-
|
|
82
54
|
return (
|
|
83
55
|
<Box w="100%" minW="600px" h={height} display="flex" flexDirection="column">
|
|
84
|
-
<Flex minH="32px" px={2} justify="flex-end" align="center" flexShrink={0}>
|
|
85
|
-
<StrategyEvidencePopover
|
|
86
|
-
timeline={evidenceTimeline}
|
|
87
|
-
showParity={showParity}
|
|
88
|
-
showRecommendations={showRecommendations}
|
|
89
|
-
onShowParityChange={setShowParity}
|
|
90
|
-
onShowRecommendationsChange={setShowRecommendations}
|
|
91
|
-
/>
|
|
92
|
-
</Flex>
|
|
93
56
|
<Box flex="1" minH={0} pr={2}>
|
|
94
57
|
{orderLog.length ? (
|
|
95
58
|
<ResponsiveContainer width="100%" height="100%">
|
|
@@ -126,24 +89,6 @@ export const RuntimeStrategyChart = ({
|
|
|
126
89
|
position: 'bottom',
|
|
127
90
|
}}
|
|
128
91
|
/>
|
|
129
|
-
{visibleEvidenceMarkers.map((marker, index) => (
|
|
130
|
-
<ReferenceLine
|
|
131
|
-
key={marker.id}
|
|
132
|
-
x={marker.timestamp}
|
|
133
|
-
stroke={markerColor(marker.type)}
|
|
134
|
-
strokeDasharray={markerDash(marker.type)}
|
|
135
|
-
strokeWidth={1.5}
|
|
136
|
-
label={{
|
|
137
|
-
value: marker.type,
|
|
138
|
-
fill: markerColor(marker.type),
|
|
139
|
-
fontSize: 10,
|
|
140
|
-
fontWeight: 600,
|
|
141
|
-
offset: 6,
|
|
142
|
-
position:
|
|
143
|
-
index % 2 === 0 ? 'insideTopRight' : 'insideTopLeft',
|
|
144
|
-
}}
|
|
145
|
-
/>
|
|
146
|
-
))}
|
|
147
92
|
<TimeSeriesXAxis
|
|
148
93
|
startTimestamp={startTimestamp}
|
|
149
94
|
endTimestamp={endTimestamp}
|
|
@@ -30,7 +30,7 @@ export const RuntimeStrategyConfigDrawer = ({
|
|
|
30
30
|
<Drawer.Content bg="gray.950">
|
|
31
31
|
<Drawer.Header>
|
|
32
32
|
<Drawer.Title>
|
|
33
|
-
{strategy.strategyName}
|
|
33
|
+
{strategy.strategyName} version v{strategy.version}
|
|
34
34
|
</Drawer.Title>
|
|
35
35
|
<Drawer.CloseTrigger asChild>
|
|
36
36
|
<CloseButton size="sm" />
|
|
@@ -38,8 +38,8 @@ export const RuntimeStrategyConfigDrawer = ({
|
|
|
38
38
|
</Drawer.Header>
|
|
39
39
|
<Drawer.Body display="flex" flexDirection="column" gap={4}>
|
|
40
40
|
<Text color="gray.400">
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
This configuration is read-only here. Change it in
|
|
42
|
+
tradejs.config.ts and deploy a new Project image.
|
|
43
43
|
</Text>
|
|
44
44
|
<Textarea
|
|
45
45
|
value={JSON.stringify(strategy.config, null, 2)}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import {
|
|
3
|
-
deleteRuntimeDeployment,
|
|
4
|
-
getRuntimeDeployment,
|
|
5
|
-
} from '@tradejs/infra/runtimeDeployments';
|
|
6
|
-
import { getCurrentUserName } from '#app/lib/currentUser';
|
|
7
|
-
|
|
8
|
-
export const DELETE = async (
|
|
9
|
-
_request: Request,
|
|
10
|
-
{ params }: { params: Promise<{ deploymentId: string }> },
|
|
11
|
-
) => {
|
|
12
|
-
const userName = await getCurrentUserName();
|
|
13
|
-
if (!userName) {
|
|
14
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
15
|
-
}
|
|
16
|
-
const { deploymentId } = await params;
|
|
17
|
-
if (!(await getRuntimeDeployment(userName, deploymentId))) {
|
|
18
|
-
return NextResponse.json(
|
|
19
|
-
{ error: 'Deployment not found' },
|
|
20
|
-
{ status: 404 },
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
await deleteRuntimeDeployment(userName, deploymentId);
|
|
24
|
-
return NextResponse.json({ deleted: true });
|
|
25
|
-
};
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
Badge,
|
|
5
|
-
Box,
|
|
6
|
-
Button,
|
|
7
|
-
Checkbox,
|
|
8
|
-
Flex,
|
|
9
|
-
Popover,
|
|
10
|
-
Portal,
|
|
11
|
-
Text,
|
|
12
|
-
} from '@chakra-ui/react';
|
|
13
|
-
import type {
|
|
14
|
-
StrategyEvidenceMarker,
|
|
15
|
-
StrategyEvidenceMarkerType,
|
|
16
|
-
StrategyEvidenceTimeline,
|
|
17
|
-
} from '@tradejs/types';
|
|
18
|
-
import { formatTimeSeriesTooltipTimestamp } from '#app/lib/timeSeriesChart';
|
|
19
|
-
|
|
20
|
-
export const STRATEGY_EVIDENCE_MARKER_PRESENTATION: Record<
|
|
21
|
-
StrategyEvidenceMarkerType,
|
|
22
|
-
{ name: string; color: string; optional: boolean }
|
|
23
|
-
> = {
|
|
24
|
-
G: { name: 'Composition / gate', color: 'purple.400', optional: false },
|
|
25
|
-
L: { name: 'MAX_LOSS_VALUE', color: 'orange.400', optional: false },
|
|
26
|
-
E: { name: 'Evidence boundary', color: 'teal.400', optional: false },
|
|
27
|
-
D: { name: 'Deployment', color: 'blue.400', optional: false },
|
|
28
|
-
P: { name: 'Runtime parity', color: 'cyan.400', optional: true },
|
|
29
|
-
R: { name: 'Recommendation', color: 'pink.400', optional: true },
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const MARKER_TYPES = Object.keys(
|
|
33
|
-
STRATEGY_EVIDENCE_MARKER_PRESENTATION,
|
|
34
|
-
) as StrategyEvidenceMarkerType[];
|
|
35
|
-
|
|
36
|
-
export const filterStrategyEvidenceMarkers = ({
|
|
37
|
-
markers,
|
|
38
|
-
showParity,
|
|
39
|
-
showRecommendations,
|
|
40
|
-
}: {
|
|
41
|
-
markers: StrategyEvidenceMarker[];
|
|
42
|
-
showParity: boolean;
|
|
43
|
-
showRecommendations: boolean;
|
|
44
|
-
}) =>
|
|
45
|
-
markers.filter(
|
|
46
|
-
(marker) =>
|
|
47
|
-
(marker.type !== 'P' || showParity) &&
|
|
48
|
-
(marker.type !== 'R' || showRecommendations),
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
const statusColor = (status: StrategyEvidenceTimeline['status']) => {
|
|
52
|
-
if (status === 'verified') return 'teal';
|
|
53
|
-
if (status === 'invalid') return 'red';
|
|
54
|
-
return 'orange';
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const uniqueProvenance = (markers: StrategyEvidenceMarker[]) => {
|
|
58
|
-
const seen = new Set<string>();
|
|
59
|
-
return markers.filter((marker) => {
|
|
60
|
-
const key = `${marker.artifactId}:${marker.artifactSha256}`;
|
|
61
|
-
if (seen.has(key)) return false;
|
|
62
|
-
seen.add(key);
|
|
63
|
-
return true;
|
|
64
|
-
});
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const FilterCheckbox = ({
|
|
68
|
-
checked,
|
|
69
|
-
label,
|
|
70
|
-
onChange,
|
|
71
|
-
}: {
|
|
72
|
-
checked: boolean;
|
|
73
|
-
label: string;
|
|
74
|
-
onChange: (checked: boolean) => void;
|
|
75
|
-
}) => (
|
|
76
|
-
<Checkbox.Root
|
|
77
|
-
size="sm"
|
|
78
|
-
checked={checked}
|
|
79
|
-
onCheckedChange={({ checked: nextChecked }) =>
|
|
80
|
-
onChange(nextChecked === true)
|
|
81
|
-
}
|
|
82
|
-
>
|
|
83
|
-
<Checkbox.HiddenInput />
|
|
84
|
-
<Checkbox.Control>
|
|
85
|
-
<Checkbox.Indicator />
|
|
86
|
-
</Checkbox.Control>
|
|
87
|
-
<Checkbox.Label>{label}</Checkbox.Label>
|
|
88
|
-
</Checkbox.Root>
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
export const StrategyEvidencePopover = ({
|
|
92
|
-
timeline,
|
|
93
|
-
showParity,
|
|
94
|
-
showRecommendations,
|
|
95
|
-
onShowParityChange,
|
|
96
|
-
onShowRecommendationsChange,
|
|
97
|
-
}: {
|
|
98
|
-
timeline: StrategyEvidenceTimeline;
|
|
99
|
-
showParity: boolean;
|
|
100
|
-
showRecommendations: boolean;
|
|
101
|
-
onShowParityChange: (checked: boolean) => void;
|
|
102
|
-
onShowRecommendationsChange: (checked: boolean) => void;
|
|
103
|
-
}) => {
|
|
104
|
-
const provenance =
|
|
105
|
-
timeline.status === 'verified' ? uniqueProvenance(timeline.markers) : [];
|
|
106
|
-
|
|
107
|
-
return (
|
|
108
|
-
<Popover.Root positioning={{ placement: 'bottom-end' }} lazyMount>
|
|
109
|
-
<Popover.Trigger asChild>
|
|
110
|
-
<Button
|
|
111
|
-
size="xs"
|
|
112
|
-
variant="outline"
|
|
113
|
-
aria-label={`Evidence: ${timeline.status}`}
|
|
114
|
-
>
|
|
115
|
-
Evidence
|
|
116
|
-
<Badge size="xs" colorPalette={statusColor(timeline.status)}>
|
|
117
|
-
{timeline.status === 'verified'
|
|
118
|
-
? `${timeline.markers.length}`
|
|
119
|
-
: timeline.status}
|
|
120
|
-
</Badge>
|
|
121
|
-
</Button>
|
|
122
|
-
</Popover.Trigger>
|
|
123
|
-
<Portal>
|
|
124
|
-
<Popover.Positioner>
|
|
125
|
-
<Popover.Content width="min(430px, calc(100vw - 24px))">
|
|
126
|
-
<Popover.Arrow />
|
|
127
|
-
<Popover.Body>
|
|
128
|
-
<Flex direction="column" gap={3}>
|
|
129
|
-
<Box>
|
|
130
|
-
<Text fontWeight="semibold">Immutable evidence</Text>
|
|
131
|
-
<Text fontSize="xs" color="gray.500">
|
|
132
|
-
{timeline.status === 'verified'
|
|
133
|
-
? timeline.observedFrom == null
|
|
134
|
-
? 'Verified artifact; no observation boundary supplied.'
|
|
135
|
-
: `Verified from ${formatTimeSeriesTooltipTimestamp(
|
|
136
|
-
timeline.observedFrom,
|
|
137
|
-
)}.`
|
|
138
|
-
: timeline.status === 'invalid'
|
|
139
|
-
? 'Evidence failed checksum, identity, or structure verification. No markers are rendered.'
|
|
140
|
-
: timeline.status === 'not_attached'
|
|
141
|
-
? 'The runtime release is published, but no checksum-verified research artifact is attached yet.'
|
|
142
|
-
: 'No checksum-verified evidence artifact is available. No mutable fallback is used.'}
|
|
143
|
-
</Text>
|
|
144
|
-
</Box>
|
|
145
|
-
|
|
146
|
-
<Box>
|
|
147
|
-
<Text fontSize="xs" fontWeight="semibold" mb={1}>
|
|
148
|
-
Legend
|
|
149
|
-
</Text>
|
|
150
|
-
<Flex wrap="wrap" gapX={3} gapY={1}>
|
|
151
|
-
{MARKER_TYPES.map((type) => (
|
|
152
|
-
<Flex key={type} align="center" gap={1}>
|
|
153
|
-
<Text
|
|
154
|
-
color={
|
|
155
|
-
STRATEGY_EVIDENCE_MARKER_PRESENTATION[type].color
|
|
156
|
-
}
|
|
157
|
-
fontWeight="bold"
|
|
158
|
-
fontSize="xs"
|
|
159
|
-
>
|
|
160
|
-
{type}
|
|
161
|
-
</Text>
|
|
162
|
-
<Text fontSize="xs" color="gray.500">
|
|
163
|
-
{STRATEGY_EVIDENCE_MARKER_PRESENTATION[type].name}
|
|
164
|
-
</Text>
|
|
165
|
-
</Flex>
|
|
166
|
-
))}
|
|
167
|
-
</Flex>
|
|
168
|
-
</Box>
|
|
169
|
-
|
|
170
|
-
<Box>
|
|
171
|
-
<Text fontSize="xs" fontWeight="semibold" mb={1}>
|
|
172
|
-
Optional markers
|
|
173
|
-
</Text>
|
|
174
|
-
<Flex gap={4}>
|
|
175
|
-
<FilterCheckbox
|
|
176
|
-
checked={showParity}
|
|
177
|
-
label="Parity (P)"
|
|
178
|
-
onChange={onShowParityChange}
|
|
179
|
-
/>
|
|
180
|
-
<FilterCheckbox
|
|
181
|
-
checked={showRecommendations}
|
|
182
|
-
label="Recommendations (R)"
|
|
183
|
-
onChange={onShowRecommendationsChange}
|
|
184
|
-
/>
|
|
185
|
-
</Flex>
|
|
186
|
-
</Box>
|
|
187
|
-
|
|
188
|
-
<Box>
|
|
189
|
-
<Text fontSize="xs" fontWeight="semibold" mb={1}>
|
|
190
|
-
Events and provenance
|
|
191
|
-
</Text>
|
|
192
|
-
{timeline.status === 'verified' && timeline.markers.length ? (
|
|
193
|
-
<Flex
|
|
194
|
-
direction="column"
|
|
195
|
-
gap={2}
|
|
196
|
-
maxH="220px"
|
|
197
|
-
overflowY="auto"
|
|
198
|
-
>
|
|
199
|
-
{timeline.markers.map((marker) => (
|
|
200
|
-
<Box key={marker.id}>
|
|
201
|
-
<Flex align="baseline" gap={2}>
|
|
202
|
-
<Text
|
|
203
|
-
color={
|
|
204
|
-
STRATEGY_EVIDENCE_MARKER_PRESENTATION[
|
|
205
|
-
marker.type
|
|
206
|
-
].color
|
|
207
|
-
}
|
|
208
|
-
fontWeight="bold"
|
|
209
|
-
fontSize="xs"
|
|
210
|
-
>
|
|
211
|
-
{marker.type}
|
|
212
|
-
</Text>
|
|
213
|
-
<Text fontSize="xs" fontWeight="semibold">
|
|
214
|
-
{marker.label}
|
|
215
|
-
</Text>
|
|
216
|
-
<Text fontSize="xs" color="gray.500">
|
|
217
|
-
{formatTimeSeriesTooltipTimestamp(
|
|
218
|
-
marker.timestamp,
|
|
219
|
-
)}
|
|
220
|
-
</Text>
|
|
221
|
-
</Flex>
|
|
222
|
-
<Text fontSize="xs" color="gray.500">
|
|
223
|
-
{marker.summary}
|
|
224
|
-
</Text>
|
|
225
|
-
</Box>
|
|
226
|
-
))}
|
|
227
|
-
</Flex>
|
|
228
|
-
) : (
|
|
229
|
-
<Text fontSize="xs" color="gray.500">
|
|
230
|
-
No verified events in the selected window.
|
|
231
|
-
</Text>
|
|
232
|
-
)}
|
|
233
|
-
</Box>
|
|
234
|
-
|
|
235
|
-
{provenance.length ? (
|
|
236
|
-
<Box>
|
|
237
|
-
<Text fontSize="xs" fontWeight="semibold" mb={1}>
|
|
238
|
-
Artifact provenance
|
|
239
|
-
</Text>
|
|
240
|
-
{provenance.map((marker) => (
|
|
241
|
-
<Text
|
|
242
|
-
key={`${marker.artifactId}:${marker.artifactSha256}`}
|
|
243
|
-
fontSize="xs"
|
|
244
|
-
color="gray.500"
|
|
245
|
-
fontFamily="mono"
|
|
246
|
-
title={marker.artifactSha256}
|
|
247
|
-
>
|
|
248
|
-
{marker.artifactId} ·{' '}
|
|
249
|
-
{marker.artifactSha256.slice(0, 12)}
|
|
250
|
-
</Text>
|
|
251
|
-
))}
|
|
252
|
-
</Box>
|
|
253
|
-
) : null}
|
|
254
|
-
</Flex>
|
|
255
|
-
</Popover.Body>
|
|
256
|
-
</Popover.Content>
|
|
257
|
-
</Popover.Positioner>
|
|
258
|
-
</Portal>
|
|
259
|
-
</Popover.Root>
|
|
260
|
-
);
|
|
261
|
-
};
|