@tradejs/app 1.0.4 → 1.0.5

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @tradejs/app
2
2
 
3
- Publishable Next.js UI package for viewing TradeJS backtests, charts, and signal flows.
3
+ Publishable Next.js UI package for the TradeJS open-source framework, with backtests, charts, and signal flows.
4
4
 
5
5
  Typical external usage:
6
6
 
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@tradejs/app",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
+ "description": "Installable Next.js UI for the TradeJS open-source framework: dashboards, backtests, charts, and runtime data.",
4
5
  "keywords": [
5
6
  "tradejs",
6
7
  "trading",
@@ -27,18 +28,26 @@
27
28
  "publishConfig": {
28
29
  "access": "public"
29
30
  },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/tradejs-dev/tradejs.git",
34
+ "directory": "apps/app"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/tradejs-dev/tradejs/issues"
38
+ },
30
39
  "dependencies": {
31
40
  "@chakra-ui/charts": "^3.24.0",
32
41
  "@chakra-ui/react": "^3.24.2",
33
42
  "@emotion/react": "^11.14.0",
34
43
  "@langchain/core": "^0.3.68",
35
44
  "@langchain/openai": "^0.6.11",
36
- "@tradejs/connectors": "^1.0.4",
37
- "@tradejs/core": "^1.0.4",
38
- "@tradejs/indicators": "^1.0.4",
39
- "@tradejs/infra": "^1.0.4",
40
- "@tradejs/node": "^1.0.4",
41
- "@tradejs/types": "^1.0.4",
45
+ "@tradejs/connectors": "^1.0.5",
46
+ "@tradejs/core": "^1.0.5",
47
+ "@tradejs/indicators": "^1.0.5",
48
+ "@tradejs/infra": "^1.0.5",
49
+ "@tradejs/node": "^1.0.5",
50
+ "@tradejs/types": "^1.0.5",
42
51
  "bcryptjs": "^2.4.3",
43
52
  "date-fns": "^3.3.1",
44
53
  "idb-keyval": "^6.2.2",
@@ -3,12 +3,36 @@ import fs from 'fs/promises';
3
3
  import path from 'path';
4
4
 
5
5
  export const runtime = 'nodejs';
6
+ export const dynamic = 'force-dynamic';
6
7
 
7
8
  const getProjectRoot = (): string => {
8
9
  const fromEnv = String(process.env.PROJECT_CWD || '').trim();
9
10
  return fromEnv ? path.resolve(fromEnv) : process.cwd();
10
11
  };
11
12
 
13
+ const stripPngExtension = (value: string) => value.replace(/\.png$/i, '');
14
+
15
+ const isSafeScreenshotName = (value: string) =>
16
+ /^[A-Za-z0-9_.-]+$/.test(value) && path.basename(value) === value;
17
+
18
+ const getScreenshotCandidates = (name: string) => {
19
+ const projectRoot = getProjectRoot();
20
+ const normalizedName = stripPngExtension(name);
21
+
22
+ return [
23
+ path.join(projectRoot, 'data', 'screenshots', `${normalizedName}.png`),
24
+ path.join(projectRoot, 'public', 'screenshots', `${normalizedName}.png`),
25
+ path.join(
26
+ projectRoot,
27
+ 'apps',
28
+ 'app',
29
+ 'public',
30
+ 'screenshots',
31
+ `${normalizedName}.png`,
32
+ ),
33
+ ];
34
+ };
35
+
12
36
  interface Params {
13
37
  name: string;
14
38
  }
@@ -17,26 +41,38 @@ export async function GET(
17
41
  _req: Request,
18
42
  { params }: { params: Promise<Params> },
19
43
  ) {
20
- try {
21
- const { name } = await params;
22
- const filePath = path.join(
23
- getProjectRoot(),
24
- 'data',
25
- 'screenshots',
26
- `${name}.png`,
44
+ const { name } = await params;
45
+
46
+ if (!isSafeScreenshotName(name)) {
47
+ return NextResponse.json(
48
+ { error: 'Invalid screenshot name' },
49
+ { status: 400 },
27
50
  );
28
- const file = await fs.readFile(filePath);
51
+ }
29
52
 
30
- const body = new Uint8Array(file);
53
+ try {
54
+ for (const filePath of getScreenshotCandidates(name)) {
55
+ try {
56
+ const file = await fs.readFile(filePath);
57
+ const body = new Uint8Array(file);
58
+
59
+ return new NextResponse(body, {
60
+ headers: {
61
+ 'Content-Type': 'image/png',
62
+ 'Content-Length': String(file.byteLength),
63
+ 'Cache-Control': 'public, max-age=60, immutable',
64
+ },
65
+ });
66
+ } catch {
67
+ continue;
68
+ }
69
+ }
31
70
 
32
- return new NextResponse(body, {
33
- headers: {
34
- 'Content-Type': 'image/png',
35
- 'Content-Length': String(file.byteLength),
36
- 'Cache-Control': 'public, max-age=60, immutable',
37
- },
38
- });
39
- } catch {
40
71
  return NextResponse.json({ error: 'Not found' }, { status: 404 });
72
+ } catch {
73
+ return NextResponse.json(
74
+ { error: 'Failed to load screenshot' },
75
+ { status: 500 },
76
+ );
41
77
  }
42
78
  }
@@ -8,11 +8,19 @@ import { KlineChart } from '../KlineChart';
8
8
 
9
9
  const DASHBOARD_REFRESH_DELAY = 10_000;
10
10
 
11
- export const MainChart = () => {
11
+ interface MainChartProps {
12
+ screenshotMode?: boolean;
13
+ }
14
+
15
+ export const MainChart = ({ screenshotMode = false }: MainChartProps) => {
12
16
  const { filters, setFilters } = useFilters();
13
17
  const { indicatorsByKey, indicatorRenderers } = useIndicators();
14
18
 
15
19
  useEffect(() => {
20
+ if (screenshotMode) {
21
+ return;
22
+ }
23
+
16
24
  const intervalId = setInterval(() => {
17
25
  setFilters({
18
26
  end: getTimestamp(),
@@ -22,7 +30,7 @@ export const MainChart = () => {
22
30
  return () => {
23
31
  clearInterval(intervalId);
24
32
  };
25
- }, [setFilters]);
33
+ }, [screenshotMode, setFilters]);
26
34
 
27
35
  return (
28
36
  <KlineChart
@@ -17,6 +17,7 @@ const Dashboard = () => {
17
17
  const hasBacktestStrategy = searchParams.has('backtestStrategy');
18
18
  const backtestId = searchParams.get('backtestId');
19
19
  const backtestStrategy = searchParams.get('backtestStrategy');
20
+ const isScreenshotMode = searchParams.get('screenshot') === '1';
20
21
 
21
22
  const parseDashboardPath = useCallback(() => {
22
23
  const parts = window.location.pathname.split('/').filter(Boolean);
@@ -89,32 +90,34 @@ const Dashboard = () => {
89
90
  <Box
90
91
  as="main"
91
92
  minH="100vh"
92
- p={4}
93
+ p={isScreenshotMode ? 0 : 4}
93
94
  bg="gray.900"
94
95
  display="flex"
95
96
  flexDirection="column"
96
97
  justifyContent="space-between"
97
98
  alignItems="flex-start"
98
99
  >
99
- <Filters.Root
100
- filters={filters}
101
- tickers={tickers}
102
- backtestFiles={tests}
103
- onChangeFilters={onChangeFilters}
104
- >
105
- <Flex mb={2} gap={4} alignItems="center" flexDirection="row">
106
- <Filters.SelectProvider />
107
- <Filters.SelectSymbol />
108
- <Filters.FavoriteIndicator />
109
- <Filters.SelectInterval />
110
- <Filters.SelectIndicator />
111
- </Flex>
112
- <Flex mb={4} gap={4} flexDirection="row">
113
- <Filters.SelectBacktest />
114
- </Flex>
115
- </Filters.Root>
100
+ {!isScreenshotMode && (
101
+ <Filters.Root
102
+ filters={filters}
103
+ tickers={tickers}
104
+ backtestFiles={tests}
105
+ onChangeFilters={onChangeFilters}
106
+ >
107
+ <Flex mb={2} gap={4} alignItems="center" flexDirection="row">
108
+ <Filters.SelectProvider />
109
+ <Filters.SelectSymbol />
110
+ <Filters.FavoriteIndicator />
111
+ <Filters.SelectInterval />
112
+ <Filters.SelectIndicator />
113
+ </Flex>
114
+ <Flex mb={4} gap={4} flexDirection="row">
115
+ <Filters.SelectBacktest />
116
+ </Flex>
117
+ </Filters.Root>
118
+ )}
116
119
  <Box position="relative" flex="1" w="full">
117
- <MainChart />
120
+ <MainChart screenshotMode={isScreenshotMode} />
118
121
  </Box>
119
122
  </Box>
120
123
  </ClientOnly>