@tradejs/app 3.1.28 → 3.1.29-beta.260

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tradejs/app",
3
- "version": "3.1.28",
3
+ "version": "3.1.29-beta.260",
4
4
  "description": "Installable Next.js UI for the TradeJS TypeScript framework: dashboards, backtests, charts, and runtime data.",
5
5
  "keywords": [
6
6
  "tradejs",
@@ -56,11 +56,11 @@
56
56
  "@chakra-ui/charts": "3.36.1",
57
57
  "@chakra-ui/react": "3.36.1",
58
58
  "@emotion/react": "^11.14.0",
59
- "@tradejs/core": "^3.1.28",
60
- "@tradejs/indicators": "^3.1.28",
61
- "@tradejs/infra": "^3.1.28",
62
- "@tradejs/node": "^3.1.28",
63
- "@tradejs/types": "^3.1.28",
59
+ "@tradejs/core": "^3.1.29-beta.260",
60
+ "@tradejs/indicators": "^3.1.29-beta.260",
61
+ "@tradejs/infra": "^3.1.29-beta.260",
62
+ "@tradejs/node": "^3.1.29-beta.260",
63
+ "@tradejs/types": "^3.1.29-beta.260",
64
64
  "@types/bcryptjs": "2.4.6",
65
65
  "@types/lodash": "4.17.24",
66
66
  "@types/node": "24.13.3",
@@ -1,3 +1,4 @@
1
+ import { JEV_PROVIDERS, normalizeJevEndpoint } from '@tradejs/core/jev';
1
2
  import bcrypt from 'bcryptjs';
2
3
  import { NextResponse } from 'next/server';
3
4
  import {
@@ -18,6 +19,10 @@ import { getCurrentUserName } from '#app/lib/currentUser';
18
19
  export const dynamic = 'force-dynamic';
19
20
 
20
21
  type UpdateBody =
22
+ | {
23
+ section: 'jev';
24
+ data?: { apiKey?: string; apiEndpoint?: string; model?: string };
25
+ }
21
26
  | {
22
27
  section: 'coinalyze';
23
28
  data?: {
@@ -58,6 +63,7 @@ const UPDATE_SECTIONS = new Set([
58
63
  'coinalyze',
59
64
  'coinmarketcap',
60
65
  'ai',
66
+ 'jev',
61
67
  'telegram',
62
68
  'password',
63
69
  ]);
@@ -106,6 +112,11 @@ const toResponse = (rawSettings: UserSettings) => {
106
112
  coinmarketcap: {
107
113
  apiKey: maskSecret(settings.COINMARKETCAP_API_KEY),
108
114
  },
115
+ jev: {
116
+ apiKey: maskSecret(settings.JEV_API_KEY || ''),
117
+ apiEndpoint: settings.JEV_API_ENDPOINT || JEV_PROVIDERS[0].endpoint,
118
+ model: settings.JEV_MODEL || JEV_PROVIDERS[0].model,
119
+ },
109
120
  ai: {
110
121
  apiKey: maskSecret(settings.AI_API_KEY),
111
122
  apiEndpoint: settings.AI_API_ENDPOINT,
@@ -199,6 +210,31 @@ export const PATCH = async (request: Request) => {
199
210
  }
200
211
  }
201
212
 
213
+ if (body.section === 'jev') {
214
+ const patch: Partial<UserRecord> = {};
215
+ if (body.data && 'apiEndpoint' in body.data) {
216
+ const endpoint = normalizeJevEndpoint(body.data.apiEndpoint);
217
+ if (!endpoint)
218
+ return NextResponse.json(
219
+ { error: 'Invalid Jev decision endpoint URL' },
220
+ { status: 400 },
221
+ );
222
+ patch.JEV_API_ENDPOINT = endpoint;
223
+ }
224
+ if (body.data && 'model' in body.data) {
225
+ const model = cleanText(body.data.model);
226
+ if (!model || model.length > 200)
227
+ return NextResponse.json(
228
+ { error: 'Jev model is required' },
229
+ { status: 400 },
230
+ );
231
+ patch.JEV_MODEL = model;
232
+ }
233
+ const apiKey = cleanOptionalText(body.data?.apiKey);
234
+ if (apiKey && !apiKey.startsWith('********')) patch.JEV_API_KEY = apiKey;
235
+ if (hasKeys(patch)) await updateUserRecord(userName, patch);
236
+ }
237
+
202
238
  if (body.section === 'ai') {
203
239
  const currentSettings = normalizeSettingsForResponse(
204
240
  await getUserSettings(userName),
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import { JEV_PROVIDERS } from '@tradejs/core/jev';
3
4
  import { useCallback, useEffect, useMemo, useState } from 'react';
4
5
  import {
5
6
  Box,
@@ -44,6 +45,7 @@ type SettingsResponse = {
44
45
  coinmarketcap: {
45
46
  apiKey: string;
46
47
  };
48
+ jev?: { apiKey: string; apiEndpoint: string; model: string };
47
49
  ai: {
48
50
  apiKey: string;
49
51
  apiEndpoint: string;
@@ -65,6 +67,9 @@ type SettingsViewState = {
65
67
  userName: string;
66
68
  coinalyzeApiKey: string;
67
69
  coinmarketcapApiKey: string;
70
+ jevApiKey: string;
71
+ jevApiEndpoint: string;
72
+ jevModel: string;
68
73
  aiApiKey: string;
69
74
  aiApiEndpoint: string;
70
75
  aiModel: string;
@@ -84,11 +89,15 @@ type SectionName =
84
89
  | 'password'
85
90
  | 'coinalyze'
86
91
  | 'coinmarketcap'
92
+ | 'jev'
87
93
  | 'ai'
88
94
  | 'telegram';
89
95
  type EditableField =
90
96
  | 'coinalyzeApiKey'
91
97
  | 'coinmarketcapApiKey'
98
+ | 'jevApiKey'
99
+ | 'jevApiEndpoint'
100
+ | 'jevModel'
92
101
  | 'aiApiKey'
93
102
  | 'aiApiEndpoint'
94
103
  | 'aiModel'
@@ -100,6 +109,9 @@ const EMPTY_SETTINGS: SettingsViewState = {
100
109
  userName: '',
101
110
  coinalyzeApiKey: '',
102
111
  coinmarketcapApiKey: '',
112
+ jevApiKey: '',
113
+ jevApiEndpoint: JEV_PROVIDERS[0].endpoint,
114
+ jevModel: JEV_PROVIDERS[0].model,
103
115
  aiApiKey: '',
104
116
  aiApiEndpoint: '',
105
117
  aiModel: '',
@@ -111,6 +123,9 @@ const EMPTY_SETTINGS: SettingsViewState = {
111
123
  const EMPTY_DRAFTS: SettingsDraftState = {
112
124
  coinalyzeApiKey: '',
113
125
  coinmarketcapApiKey: '',
126
+ jevApiKey: '',
127
+ jevApiEndpoint: JEV_PROVIDERS[0].endpoint,
128
+ jevModel: JEV_PROVIDERS[0].model,
114
129
  aiApiKey: '',
115
130
  aiApiEndpoint: '',
116
131
  aiModel: '',
@@ -127,6 +142,9 @@ const EMPTY_PASSWORDS: PasswordState = {
127
142
  const EMPTY_EDITING: Record<EditableField, boolean> = {
128
143
  coinalyzeApiKey: false,
129
144
  coinmarketcapApiKey: false,
145
+ jevApiKey: false,
146
+ jevApiEndpoint: false,
147
+ jevModel: false,
130
148
  aiApiKey: false,
131
149
  aiApiEndpoint: false,
132
150
  aiModel: false,
@@ -139,6 +157,7 @@ const MASKED_FIELDS = new Set<EditableField>([
139
157
  'coinalyzeApiKey',
140
158
  'coinmarketcapApiKey',
141
159
  'aiApiKey',
160
+ 'jevApiKey',
142
161
  'tgBotToken',
143
162
  ]);
144
163
 
@@ -146,6 +165,10 @@ const toViewState = (payload: SettingsResponse): SettingsViewState => ({
146
165
  userName: payload.userName,
147
166
  coinalyzeApiKey: payload.settings.coinalyze.apiKey || '',
148
167
  coinmarketcapApiKey: payload.settings.coinmarketcap.apiKey || '',
168
+ jevApiKey: payload.settings.jev?.apiKey || '',
169
+ jevApiEndpoint:
170
+ payload.settings.jev?.apiEndpoint || JEV_PROVIDERS[0].endpoint,
171
+ jevModel: payload.settings.jev?.model || JEV_PROVIDERS[0].model,
149
172
  aiApiKey: payload.settings.ai.apiKey || '',
150
173
  aiApiEndpoint:
151
174
  normalizeAiEndpoint(payload.settings.ai.apiEndpoint) ||
@@ -167,6 +190,8 @@ const toViewState = (payload: SettingsResponse): SettingsViewState => ({
167
190
 
168
191
  const toDraftState = (view: SettingsViewState): SettingsDraftState => ({
169
192
  ...EMPTY_DRAFTS,
193
+ jevApiEndpoint: view.jevApiEndpoint,
194
+ jevModel: view.jevModel,
170
195
  aiApiEndpoint: view.aiApiEndpoint,
171
196
  aiModel: view.aiModel,
172
197
  aiResponseLanguage: view.aiResponseLanguage,
@@ -304,6 +329,13 @@ export const AccountSettingsDrawer = () => {
304
329
  );
305
330
  }
306
331
 
332
+ if (section === 'jev')
333
+ return (
334
+ Boolean(drafts.jevApiKey.trim()) ||
335
+ drafts.jevApiEndpoint !== settings.jevApiEndpoint ||
336
+ drafts.jevModel !== settings.jevModel
337
+ );
338
+
307
339
  if (section === 'coinalyze') {
308
340
  return Boolean(drafts.coinalyzeApiKey.trim());
309
341
  }
@@ -405,48 +437,57 @@ export const AccountSettingsDrawer = () => {
405
437
 
406
438
  try {
407
439
  const body =
408
- section === 'coinalyze'
440
+ section === 'jev'
409
441
  ? {
410
442
  section,
411
443
  data: {
412
- apiKey: getSecretUpdateValue('coinalyzeApiKey'),
444
+ apiKey: getSecretUpdateValue('jevApiKey'),
445
+ apiEndpoint: drafts.jevApiEndpoint,
446
+ model: drafts.jevModel.trim(),
413
447
  },
414
448
  }
415
- : section === 'ai'
449
+ : section === 'coinalyze'
416
450
  ? {
417
451
  section,
418
452
  data: {
419
- apiKey: getSecretUpdateValue('aiApiKey'),
420
- apiEndpoint: drafts.aiApiEndpoint,
421
- model: drafts.aiModel.trim(),
422
- responseLanguage: drafts.aiResponseLanguage,
453
+ apiKey: getSecretUpdateValue('coinalyzeApiKey'),
423
454
  },
424
455
  }
425
- : section === 'coinmarketcap'
456
+ : section === 'ai'
426
457
  ? {
427
458
  section,
428
459
  data: {
429
- apiKey: getSecretUpdateValue('coinmarketcapApiKey'),
460
+ apiKey: getSecretUpdateValue('aiApiKey'),
461
+ apiEndpoint: drafts.aiApiEndpoint,
462
+ model: drafts.aiModel.trim(),
463
+ responseLanguage: drafts.aiResponseLanguage,
430
464
  },
431
465
  }
432
- : section === 'telegram'
466
+ : section === 'coinmarketcap'
433
467
  ? {
434
468
  section,
435
469
  data: {
436
- botToken: getSecretUpdateValue('tgBotToken'),
437
- chatId:
438
- drafts.tgChatId !== settings.tgChatId
439
- ? drafts.tgChatId.trim()
440
- : undefined,
470
+ apiKey: getSecretUpdateValue('coinmarketcapApiKey'),
441
471
  },
442
472
  }
443
- : {
444
- section,
445
- data: {
446
- password: passwords.password,
447
- confirmPassword: passwords.confirmPassword,
448
- },
449
- };
473
+ : section === 'telegram'
474
+ ? {
475
+ section,
476
+ data: {
477
+ botToken: getSecretUpdateValue('tgBotToken'),
478
+ chatId:
479
+ drafts.tgChatId !== settings.tgChatId
480
+ ? drafts.tgChatId.trim()
481
+ : undefined,
482
+ },
483
+ }
484
+ : {
485
+ section,
486
+ data: {
487
+ password: passwords.password,
488
+ confirmPassword: passwords.confirmPassword,
489
+ },
490
+ };
450
491
 
451
492
  const response = await fetch('/api/user/settings', {
452
493
  method: 'PATCH',
@@ -660,6 +701,96 @@ export const AccountSettingsDrawer = () => {
660
701
  </Stack>
661
702
  </Box>
662
703
 
704
+ <Box
705
+ borderWidth="1px"
706
+ borderColor="gray.700"
707
+ borderRadius="lg"
708
+ p={4}
709
+ bg="gray.900"
710
+ >
711
+ <Stack gap={4}>
712
+ <Box>
713
+ <Text fontWeight="600">Jev</Text>
714
+ <Text fontSize="sm" color="gray.400">
715
+ Evaluate signal context and geometry in backtests and
716
+ runtime.
717
+ </Text>
718
+ </Box>
719
+ <Field.Root>
720
+ <Field.Label>Jev provider</Field.Label>
721
+ <select
722
+ aria-label="Jev provider"
723
+ style={DRAWER_SELECT_STYLE}
724
+ value={
725
+ JEV_PROVIDERS.find(
726
+ (item) => item.endpoint === drafts.jevApiEndpoint,
727
+ )?.endpoint ?? 'custom'
728
+ }
729
+ onChange={(event) => {
730
+ const provider = JEV_PROVIDERS.find(
731
+ (item) => item.endpoint === event.target.value,
732
+ );
733
+ setDrafts((current) => ({
734
+ ...current,
735
+ jevApiEndpoint: provider?.endpoint ?? '',
736
+ jevModel: provider?.model ?? '',
737
+ }));
738
+ }}
739
+ >
740
+ {JEV_PROVIDERS.map((provider) => (
741
+ <option
742
+ key={provider.endpoint}
743
+ value={provider.endpoint}
744
+ >
745
+ {provider.label}
746
+ </option>
747
+ ))}
748
+ <option value="custom">Custom Decisions API</option>
749
+ </select>
750
+ </Field.Root>
751
+ <Field.Root>
752
+ <Field.Label>JEV_API_ENDPOINT</Field.Label>
753
+ <Input
754
+ aria-label="JEV_API_ENDPOINT"
755
+ value={drafts.jevApiEndpoint}
756
+ onChange={(event) =>
757
+ updateDraft('jevApiEndpoint', event.target.value)
758
+ }
759
+ placeholder="Full URL of the decision endpoint"
760
+ />
761
+ <Text fontSize="sm" color="gray.400">
762
+ Use a TypeSafe-compatible decision endpoint accepting
763
+ state and questions.
764
+ </Text>
765
+ </Field.Root>
766
+ <Field.Root>
767
+ <Field.Label>JEV_MODEL</Field.Label>
768
+ <Input
769
+ aria-label="JEV_MODEL"
770
+ value={drafts.jevModel}
771
+ onChange={(event) =>
772
+ updateDraft('jevModel', event.target.value)
773
+ }
774
+ />
775
+ </Field.Root>
776
+ {renderEditableField({
777
+ label: 'JEV_API_KEY',
778
+ field: 'jevApiKey',
779
+ placeholder: 'Enter a new Jev provider API key',
780
+ })}
781
+ <Flex justify="flex-end">
782
+ <Button
783
+ colorPalette="teal"
784
+ loading={savingSection === 'jev'}
785
+ disabled={!isSectionDirty('jev')}
786
+ onClick={() => saveSection('jev')}
787
+ >
788
+ Save
789
+ </Button>
790
+ </Flex>
791
+ </Stack>
792
+ </Box>
793
+
663
794
  <Box
664
795
  borderWidth="1px"
665
796
  borderColor="gray.700"