@roastcodes/ttdash 6.1.3 → 6.1.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/dist/assets/{CustomTooltip-bxoGwmdb.js → CustomTooltip-Be-rHcDB.js} +1 -1
- package/dist/assets/{DrillDownModal-Df1554cO.js → DrillDownModal-DXP44-00.js} +1 -1
- package/dist/assets/index-TppJ6Iqj.css +2 -0
- package/dist/assets/{index-Dfbgu597.js → index-_318nw_j.js} +3 -3
- package/dist/index.html +3 -3
- package/package.json +1 -2
- package/server/report/charts.js +29 -15
- package/server/report/index.js +129 -109
- package/server/report/utils.js +209 -18
- package/server.js +11 -11
- package/src/locales/de/common.json +64 -1
- package/src/locales/en/common.json +64 -1
- package/dist/assets/index-DWoj-vpZ.css +0 -2
package/dist/index.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<link rel="icon" type="image/png" sizes="256x256" href="/favicon.png" />
|
|
9
9
|
<link rel="shortcut icon" href="/favicon.png" />
|
|
10
10
|
<link rel="apple-touch-icon" href="/favicon.png" />
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-_318nw_j.js"></script>
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-COnpUsM8.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-CiBqdKXh.js">
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/react-vendor-0R1rd57Z.js">
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
<link rel="modulepreload" crossorigin href="/assets/icons-vendor-DFoaijFJ.js">
|
|
18
18
|
<link rel="modulepreload" crossorigin href="/assets/dialog-Cn1m7WhC.js">
|
|
19
19
|
<link rel="modulepreload" crossorigin href="/assets/button-D7Ib8H7t.js">
|
|
20
|
-
<link rel="modulepreload" crossorigin href="/assets/CustomTooltip-
|
|
21
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
20
|
+
<link rel="modulepreload" crossorigin href="/assets/CustomTooltip-Be-rHcDB.js">
|
|
21
|
+
<link rel="stylesheet" crossorigin href="/assets/index-TppJ6Iqj.css">
|
|
22
22
|
</head>
|
|
23
23
|
<body>
|
|
24
24
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roastcodes/ttdash",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.5",
|
|
4
4
|
"description": "Local-first dashboard and CLI for toktrack usage data",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"repository": {
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
"test:e2e:ci": "playwright test",
|
|
29
29
|
"test:all": "npm run test:unit && npm run test:e2e",
|
|
30
30
|
"pack:dry-run": "npm pack --dry-run",
|
|
31
|
-
"tag:main-release": "bash scripts/tag-main-release.sh",
|
|
32
31
|
"verify:package": "node scripts/verify-package.js",
|
|
33
32
|
"verify:registry-install": "node scripts/verify-registry-install.js",
|
|
34
33
|
"verify:release": "npm run test:unit:coverage && npm run build && npm run verify:package",
|
package/server/report/charts.js
CHANGED
|
@@ -17,7 +17,15 @@ ${body}
|
|
|
17
17
|
</svg>`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
const DEFAULT_FONT_FAMILY = 'Liberation Sans, DejaVu Sans, Arial, sans-serif';
|
|
21
|
+
|
|
22
|
+
function truncateSvgLabel(value, maxLength = 28) {
|
|
23
|
+
const stringValue = String(value || '');
|
|
24
|
+
if (stringValue.length <= maxLength) return stringValue;
|
|
25
|
+
return `${stringValue.slice(0, Math.max(1, maxLength - 1)).trimEnd()}…`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function lineChart(data, { valueKey, secondaryKey, title, stroke = '#1f6feb', fill = 'rgba(31, 111, 235, 0.14)', formatter = (value) => String(value), fontFamily = DEFAULT_FONT_FAMILY }) {
|
|
21
29
|
const width = 980;
|
|
22
30
|
const height = 360;
|
|
23
31
|
const margin = { top: 42, right: 28, bottom: 54, left: 74 };
|
|
@@ -53,10 +61,10 @@ function lineChart(data, { valueKey, secondaryKey, title, stroke = '#1f6feb', fi
|
|
|
53
61
|
|
|
54
62
|
return svgDoc(width, height, `
|
|
55
63
|
<rect width="${width}" height="${height}" rx="24" fill="#ffffff"/>
|
|
56
|
-
<text x="${margin.left}" y="26" font-size="18" font-family="
|
|
64
|
+
<text x="${margin.left}" y="26" font-size="18" font-family="${fontFamily}" font-weight="700" fill="#122033">${escapeXml(title)}</text>
|
|
57
65
|
${yTicks.map((tick) => `
|
|
58
66
|
<line x1="${margin.left}" y1="${tick.y}" x2="${margin.left + plotWidth}" y2="${tick.y}" stroke="#e6edf5" stroke-width="1"/>
|
|
59
|
-
<text x="${margin.left - 12}" y="${tick.y + 4}" text-anchor="end" font-size="11" font-family="
|
|
67
|
+
<text x="${margin.left - 12}" y="${tick.y + 4}" text-anchor="end" font-size="11" font-family="${fontFamily}" fill="#5c6b7e">${escapeXml(formatter(tick.value))}</text>
|
|
60
68
|
`).join('')}
|
|
61
69
|
<line x1="${margin.left}" y1="${margin.top}" x2="${margin.left}" y2="${margin.top + plotHeight}" stroke="#98a6b7" stroke-width="1.2"/>
|
|
62
70
|
<line x1="${margin.left}" y1="${margin.top + plotHeight}" x2="${margin.left + plotWidth}" y2="${margin.top + plotHeight}" stroke="#98a6b7" stroke-width="1.2"/>
|
|
@@ -69,15 +77,21 @@ function lineChart(data, { valueKey, secondaryKey, title, stroke = '#1f6feb', fi
|
|
|
69
77
|
<circle cx="${x(index)}" cy="${y(value)}" r="${data.length > 40 ? 0 : 3.8}" fill="${stroke}"/>
|
|
70
78
|
`).join('')}
|
|
71
79
|
${data.map((entry, index) => index % labelStep === 0 || index === data.length - 1 ? `
|
|
72
|
-
<text x="${x(index)}" y="${height - 18}" text-anchor="middle" font-size="11" font-family="
|
|
80
|
+
<text x="${x(index)}" y="${height - 18}" text-anchor="middle" font-size="11" font-family="${fontFamily}" fill="#5c6b7e">${escapeXml(entry.label)}</text>
|
|
73
81
|
` : '').join('')}
|
|
74
82
|
`);
|
|
75
83
|
}
|
|
76
84
|
|
|
77
|
-
function horizontalBarChart(data, { title, formatter = (value) => String(value), getValue, getLabel, getColor }) {
|
|
85
|
+
function horizontalBarChart(data, { title, formatter = (value) => String(value), getValue, getLabel, getColor, fontFamily = DEFAULT_FONT_FAMILY }) {
|
|
78
86
|
const width = 980;
|
|
79
87
|
const height = 360;
|
|
80
|
-
const
|
|
88
|
+
const longestLabelLength = data.reduce((max, entry) => Math.max(max, String(getLabel(entry) || '').length), 0);
|
|
89
|
+
const margin = {
|
|
90
|
+
top: 46,
|
|
91
|
+
right: 100,
|
|
92
|
+
bottom: 24,
|
|
93
|
+
left: clamp(180 + longestLabelLength * 3.4, 220, 320),
|
|
94
|
+
};
|
|
81
95
|
const plotWidth = width - margin.left - margin.right;
|
|
82
96
|
const barGap = 18;
|
|
83
97
|
const barHeight = Math.min(28, (height - margin.top - margin.bottom - barGap * (data.length - 1)) / Math.max(data.length, 1));
|
|
@@ -85,22 +99,22 @@ function horizontalBarChart(data, { title, formatter = (value) => String(value),
|
|
|
85
99
|
|
|
86
100
|
return svgDoc(width, height, `
|
|
87
101
|
<rect width="${width}" height="${height}" rx="24" fill="#ffffff"/>
|
|
88
|
-
<text x="${margin.left}" y="28" font-size="18" font-family="
|
|
102
|
+
<text x="${margin.left}" y="28" font-size="18" font-family="${fontFamily}" font-weight="700" fill="#122033">${escapeXml(title)}</text>
|
|
89
103
|
${data.map((entry, index) => {
|
|
90
104
|
const y = margin.top + index * (barHeight + barGap);
|
|
91
105
|
const value = getValue(entry);
|
|
92
106
|
const barWidth = clamp((value / maxValue) * plotWidth, 0, plotWidth);
|
|
93
107
|
return `
|
|
94
|
-
<text x="${margin.left - 18}" y="${y + barHeight / 2 + 4}" text-anchor="end" font-size="13" font-family="
|
|
108
|
+
<text x="${margin.left - 18}" y="${y + barHeight / 2 + 4}" text-anchor="end" font-size="13" font-family="${fontFamily}" fill="#122033">${escapeXml(truncateSvgLabel(getLabel(entry), 30))}</text>
|
|
95
109
|
<rect x="${margin.left}" y="${y}" width="${plotWidth}" height="${barHeight}" rx="12" fill="#eef3f8"/>
|
|
96
110
|
<rect x="${margin.left}" y="${y}" width="${barWidth}" height="${barHeight}" rx="12" fill="${getColor(entry)}"/>
|
|
97
|
-
<text x="${margin.left + plotWidth + 12}" y="${y + barHeight / 2 + 4}" font-size="12" font-family="
|
|
111
|
+
<text x="${margin.left + plotWidth + 12}" y="${y + barHeight / 2 + 4}" font-size="12" font-family="${fontFamily}" fill="#475569">${escapeXml(formatter(value))}</text>
|
|
98
112
|
`;
|
|
99
113
|
}).join('')}
|
|
100
114
|
`);
|
|
101
115
|
}
|
|
102
116
|
|
|
103
|
-
function stackedBarChart(data, { title, segments }) {
|
|
117
|
+
function stackedBarChart(data, { title, segments, formatter = (value) => String(value), fontFamily = DEFAULT_FONT_FAMILY }) {
|
|
104
118
|
const width = 980;
|
|
105
119
|
const height = 380;
|
|
106
120
|
const margin = { top: 52, right: 30, bottom: 56, left: 74 };
|
|
@@ -114,13 +128,13 @@ function stackedBarChart(data, { title, segments }) {
|
|
|
114
128
|
|
|
115
129
|
return svgDoc(width, height, `
|
|
116
130
|
<rect width="${width}" height="${height}" rx="24" fill="#ffffff"/>
|
|
117
|
-
<text x="${margin.left}" y="30" font-size="18" font-family="
|
|
131
|
+
<text x="${margin.left}" y="30" font-size="18" font-family="${fontFamily}" font-weight="700" fill="#122033">${escapeXml(title)}</text>
|
|
118
132
|
${Array.from({ length: 5 }, (_, index) => {
|
|
119
133
|
const value = (maxValue / 4) * index;
|
|
120
134
|
const y = margin.top + plotHeight - (value / maxValue) * plotHeight;
|
|
121
135
|
return `
|
|
122
136
|
<line x1="${margin.left}" y1="${y}" x2="${margin.left + plotWidth}" y2="${y}" stroke="#e6edf5" stroke-width="1"/>
|
|
123
|
-
<text x="${margin.left - 12}" y="${y + 4}" text-anchor="end" font-size="11" font-family="
|
|
137
|
+
<text x="${margin.left - 12}" y="${y + 4}" text-anchor="end" font-size="11" font-family="${fontFamily}" fill="#5c6b7e">${escapeXml(formatter(value))}</text>
|
|
124
138
|
`;
|
|
125
139
|
}).join('')}
|
|
126
140
|
${data.map((entry, index) => {
|
|
@@ -134,13 +148,13 @@ function stackedBarChart(data, { title, segments }) {
|
|
|
134
148
|
return `<rect x="${x}" y="${y}" width="${barWidth}" height="${h}" rx="6" fill="${segment.color}"/>`;
|
|
135
149
|
}).join('');
|
|
136
150
|
const label = index % labelStep === 0 || index === data.length - 1
|
|
137
|
-
? `<text x="${x + barWidth / 2}" y="${height - 18}" text-anchor="middle" font-size="11" font-family="
|
|
151
|
+
? `<text x="${x + barWidth / 2}" y="${height - 18}" text-anchor="middle" font-size="11" font-family="${fontFamily}" fill="#5c6b7e">${escapeXml(entry.label)}</text>`
|
|
138
152
|
: '';
|
|
139
153
|
return `${rects}${label}`;
|
|
140
154
|
}).join('')}
|
|
141
155
|
${segments.map((segment, index) => `
|
|
142
|
-
<rect x="${margin.left + index * 156}" y="${height - 34}" width="12" height="12" rx="3" fill="${segment.color}"/>
|
|
143
|
-
<text x="${margin.left + 18 + index * 156}" y="${height - 24}" font-size="11" font-family="
|
|
156
|
+
<rect x="${margin.left + (index % 3) * 156}" y="${height - 34 - Math.floor(index / 3) * 18}" width="12" height="12" rx="3" fill="${segment.color}"/>
|
|
157
|
+
<text x="${margin.left + 18 + (index % 3) * 156}" y="${height - 24 - Math.floor(index / 3) * 18}" font-size="11" font-family="${fontFamily}" fill="#334155">${escapeXml(segment.label)}</text>
|
|
144
158
|
`).join('')}
|
|
145
159
|
`);
|
|
146
160
|
}
|
package/server/report/index.js
CHANGED
|
@@ -2,7 +2,8 @@ const fs = require('fs');
|
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { spawn } = require('child_process');
|
|
5
|
-
const { buildReportData, formatDateAxis } = require('./utils');
|
|
5
|
+
const { buildReportData, formatCompactAxis, formatDateAxis } = require('./utils');
|
|
6
|
+
const { translate } = require('./i18n');
|
|
6
7
|
const { horizontalBarChart, lineChart, stackedBarChart } = require('./charts');
|
|
7
8
|
|
|
8
9
|
function ensureTypstInstalled() {
|
|
@@ -33,7 +34,7 @@ function compileTypst(workingDir, typPath, pdfPath) {
|
|
|
33
34
|
resolve();
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
36
|
-
reject(new Error(stderr.trim() || '
|
|
37
|
+
reject(new Error(`Typst compilation failed: ${stderr.trim() || 'unknown error'}`));
|
|
37
38
|
});
|
|
38
39
|
});
|
|
39
40
|
}
|
|
@@ -48,7 +49,7 @@ function buildTemplate() {
|
|
|
48
49
|
fill: rgb("#f3f6f9"),
|
|
49
50
|
)
|
|
50
51
|
|
|
51
|
-
#set text(font: "Arial", lang: if report.meta.language == "en" { "en" } else { "de" })
|
|
52
|
+
#set text(font: ("Liberation Sans", "DejaVu Sans", "Arial"), lang: if report.meta.language == "en" { "en" } else { "de" })
|
|
52
53
|
#set par(justify: false, leading: 0.55em)
|
|
53
54
|
|
|
54
55
|
#let ink = rgb("#102132")
|
|
@@ -76,6 +77,18 @@ function buildTemplate() {
|
|
|
76
77
|
],
|
|
77
78
|
)
|
|
78
79
|
|
|
80
|
+
#let insight-card(title, body, tone: accent) = rect(
|
|
81
|
+
inset: 11pt,
|
|
82
|
+
radius: 14pt,
|
|
83
|
+
fill: panel,
|
|
84
|
+
stroke: (paint: line, thickness: 0.8pt),
|
|
85
|
+
[
|
|
86
|
+
#text(size: 9pt, fill: tone, weight: "bold")[#title]
|
|
87
|
+
#v(4pt)
|
|
88
|
+
#text(size: 9.6pt, fill: ink)[#body]
|
|
89
|
+
],
|
|
90
|
+
)
|
|
91
|
+
|
|
79
92
|
#show heading.where(level: 1): it => block(above: 0pt, below: 10pt)[
|
|
80
93
|
#text(size: 24pt, fill: ink, weight: "bold")[#it.body]
|
|
81
94
|
]
|
|
@@ -91,7 +104,7 @@ function buildTemplate() {
|
|
|
91
104
|
|
|
92
105
|
#box(fill: rgb("#f0f6ff"), inset: 16pt, radius: 18pt, width: 100%)[
|
|
93
106
|
#align(left)[
|
|
94
|
-
#text(size: 9pt, fill: accent, weight: "bold")[
|
|
107
|
+
#text(size: 9pt, fill: accent, weight: "bold")[#report.text.headerEyebrow]
|
|
95
108
|
#v(6pt)
|
|
96
109
|
#text(size: 26pt, fill: ink, weight: "bold")[#report.meta.reportTitle]
|
|
97
110
|
#v(4pt)
|
|
@@ -100,9 +113,9 @@ function buildTemplate() {
|
|
|
100
113
|
#grid(
|
|
101
114
|
columns: (1fr, 1fr, 1fr),
|
|
102
115
|
gutter: 8pt,
|
|
103
|
-
metric-card(
|
|
104
|
-
metric-card(
|
|
105
|
-
metric-card(
|
|
116
|
+
metric-card(report.text.fields.dateRange, report.labels.dateRangeText),
|
|
117
|
+
metric-card(report.text.fields.view, report.meta.filterSummary.viewMode),
|
|
118
|
+
metric-card(report.text.fields.generated, report.meta.generatedAtLabel),
|
|
106
119
|
)
|
|
107
120
|
]
|
|
108
121
|
]
|
|
@@ -115,9 +128,24 @@ function buildTemplate() {
|
|
|
115
128
|
..report.summaryCards.map(card => metric-card(card.label, card.value, note: card.note, tone: if card.tone == "warn" { warn } else if card.tone == "good" { good } else { accent })),
|
|
116
129
|
)
|
|
117
130
|
|
|
131
|
+
#if report.insights.items.len() > 0 [
|
|
132
|
+
#v(12pt)
|
|
133
|
+
= #report.text.sections.insights
|
|
134
|
+
|
|
135
|
+
#grid(
|
|
136
|
+
columns: (1fr, 1fr),
|
|
137
|
+
gutter: 8pt,
|
|
138
|
+
..report.insights.items.map(item => insight-card(
|
|
139
|
+
item.title,
|
|
140
|
+
item.body,
|
|
141
|
+
tone: if item.tone == "warn" { warn } else if item.tone == "good" { good } else { accent },
|
|
142
|
+
)),
|
|
143
|
+
)
|
|
144
|
+
]
|
|
145
|
+
|
|
118
146
|
#v(12pt)
|
|
119
147
|
|
|
120
|
-
= #
|
|
148
|
+
= #report.text.sections.overview
|
|
121
149
|
|
|
122
150
|
#grid(
|
|
123
151
|
columns: (1fr, 1fr),
|
|
@@ -136,107 +164,95 @@ function buildTemplate() {
|
|
|
136
164
|
#image("token-trend.svg", width: 100%)
|
|
137
165
|
]
|
|
138
166
|
|
|
167
|
+
#pagebreak()
|
|
168
|
+
|
|
139
169
|
#v(12pt)
|
|
140
170
|
|
|
141
|
-
= #
|
|
171
|
+
= #report.text.sections.filters
|
|
142
172
|
|
|
143
173
|
#grid(
|
|
144
|
-
columns: (1fr, 1fr
|
|
174
|
+
columns: (1fr, 1fr),
|
|
145
175
|
gutter: 8pt,
|
|
146
|
-
metric-card(
|
|
147
|
-
metric-card(
|
|
148
|
-
metric-card(
|
|
149
|
-
metric-card(
|
|
150
|
-
metric-card(
|
|
176
|
+
metric-card(report.text.fields.month, report.meta.filterSummary.selectedMonthLabel),
|
|
177
|
+
metric-card(report.text.fields.selectedProviders, report.meta.filterSummary.selectedProvidersLabel),
|
|
178
|
+
metric-card(report.text.fields.selectedModels, report.meta.filterSummary.selectedModelsLabel),
|
|
179
|
+
metric-card(report.text.fields.startDate, report.meta.filterSummary.startDateLabel),
|
|
180
|
+
metric-card(report.text.fields.endDate, report.meta.filterSummary.endDateLabel),
|
|
151
181
|
)
|
|
152
182
|
|
|
153
183
|
#v(10pt)
|
|
154
184
|
|
|
155
|
-
= #
|
|
185
|
+
= #report.text.sections.modelsProviders
|
|
186
|
+
|
|
187
|
+
#if report.topModels.len() > 0 or report.providers.len() > 0 [
|
|
188
|
+
#grid(
|
|
189
|
+
columns: (1fr, 1fr),
|
|
190
|
+
gutter: 10pt,
|
|
191
|
+
rect(inset: 10pt, radius: 14pt, fill: panel, stroke: (paint: line, thickness: 0.8pt))[
|
|
192
|
+
#text(size: 12pt, weight: "bold", fill: ink)[#report.text.tables.topModels]
|
|
193
|
+
#v(6pt)
|
|
194
|
+
#set text(size: 8.8pt)
|
|
195
|
+
#table(
|
|
196
|
+
columns: (2.2fr, 1.4fr, 1fr, 0.9fr),
|
|
197
|
+
column-gutter: 8pt,
|
|
198
|
+
align: (x, y) => if x < 2 { left } else { right },
|
|
199
|
+
table.header([*#report.text.tables.columns.model*], [*#report.text.tables.columns.provider*], [*#report.text.tables.columns.cost*], [*#report.text.tables.columns.requests*]),
|
|
200
|
+
..report.topModels.map(model => (
|
|
201
|
+
[#model.name],
|
|
202
|
+
[#model.provider],
|
|
203
|
+
[#model.costLabel],
|
|
204
|
+
[#model.requestsLabel],
|
|
205
|
+
)).flatten(),
|
|
206
|
+
)
|
|
207
|
+
],
|
|
208
|
+
rect(inset: 10pt, radius: 14pt, fill: panel, stroke: (paint: line, thickness: 0.8pt))[
|
|
209
|
+
#text(size: 12pt, weight: "bold", fill: ink)[#report.text.tables.providers]
|
|
210
|
+
#v(6pt)
|
|
211
|
+
#set text(size: 8.8pt)
|
|
212
|
+
#table(
|
|
213
|
+
columns: (1.8fr, 1fr, 1fr, 1fr),
|
|
214
|
+
column-gutter: 8pt,
|
|
215
|
+
align: (x, y) => if x == 0 { left } else { right },
|
|
216
|
+
table.header([*#report.text.tables.columns.provider*], [*#report.text.tables.columns.cost*], [*#report.text.tables.columns.tokens*], [*#report.text.tables.columns.requests*]),
|
|
217
|
+
..report.providers.map(provider => (
|
|
218
|
+
[#provider.name],
|
|
219
|
+
[#provider.costLabel],
|
|
220
|
+
[#provider.tokensLabel],
|
|
221
|
+
[#provider.requestsLabel],
|
|
222
|
+
)).flatten(),
|
|
223
|
+
)
|
|
224
|
+
],
|
|
225
|
+
)
|
|
226
|
+
]
|
|
156
227
|
|
|
157
|
-
#
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
rect(inset: 10pt, radius: 14pt, fill: panel, stroke: (paint: line, thickness: 0.8pt))[
|
|
161
|
-
#text(size:
|
|
162
|
-
#v(6pt)
|
|
163
|
-
#set text(size: 8.8pt)
|
|
164
|
-
#table(
|
|
165
|
-
columns: (2.5fr, 1.5fr, 1fr, 1fr),
|
|
166
|
-
column-gutter: 8pt,
|
|
167
|
-
align: (x, y) => if x < 2 { left } else { right },
|
|
168
|
-
table.header([*#if report.meta.language == "en" { "Model" } else { "Modell" }*], [*Provider*], [*#if report.meta.language == "en" { "Cost" } else { "Kosten" }*], [*Requests*]),
|
|
169
|
-
..report.topModels.map(model => (
|
|
170
|
-
[#model.name],
|
|
171
|
-
[#model.provider],
|
|
172
|
-
[#model.costLabel],
|
|
173
|
-
[#model.requestsLabel],
|
|
174
|
-
)).flatten(),
|
|
175
|
-
)
|
|
176
|
-
],
|
|
177
|
-
rect(inset: 10pt, radius: 14pt, fill: panel, stroke: (paint: line, thickness: 0.8pt))[
|
|
178
|
-
#text(size: 12pt, weight: "bold", fill: ink)[#if report.meta.language == "en" { "Providers" } else { "Provider" }]
|
|
179
|
-
#v(6pt)
|
|
180
|
-
#set text(size: 8.8pt)
|
|
228
|
+
#if report.recentPeriods.len() > 0 [
|
|
229
|
+
= #report.text.sections.recentPeriods
|
|
230
|
+
|
|
231
|
+
#rect(inset: 10pt, radius: 14pt, fill: panel, stroke: (paint: line, thickness: 0.8pt))[
|
|
232
|
+
#set text(size: 8.9pt)
|
|
181
233
|
#table(
|
|
182
|
-
columns: (
|
|
234
|
+
columns: (2fr, 1fr, 1fr, 1fr),
|
|
183
235
|
column-gutter: 8pt,
|
|
184
236
|
align: (x, y) => if x == 0 { left } else { right },
|
|
185
|
-
table.header([*
|
|
186
|
-
..report.
|
|
187
|
-
[#
|
|
188
|
-
[#
|
|
189
|
-
[#
|
|
190
|
-
[#
|
|
237
|
+
table.header([*#report.text.tables.columns.period*], [*#report.text.tables.columns.cost*], [*#report.text.tables.columns.tokens*], [*#report.text.tables.columns.requests*]),
|
|
238
|
+
..report.recentPeriods.map(item => (
|
|
239
|
+
[#item.label],
|
|
240
|
+
[#item.costLabel],
|
|
241
|
+
[#item.tokensLabel],
|
|
242
|
+
[#item.requestsLabel],
|
|
191
243
|
)).flatten(),
|
|
192
244
|
)
|
|
193
|
-
]
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
#pagebreak()
|
|
197
|
-
|
|
198
|
-
= #if report.meta.language == "en" { "Recent periods" } else { "Letzte Zeiträume" }
|
|
199
|
-
|
|
200
|
-
#rect(inset: 10pt, radius: 14pt, fill: panel, stroke: (paint: line, thickness: 0.8pt))[
|
|
201
|
-
#set text(size: 8.9pt)
|
|
202
|
-
#table(
|
|
203
|
-
columns: (2fr, 1fr, 1fr, 1fr),
|
|
204
|
-
column-gutter: 8pt,
|
|
205
|
-
align: (x, y) => if x == 0 { left } else { right },
|
|
206
|
-
table.header([*#if report.meta.language == "en" { "Period" } else { "Zeitraum" }*], [*#if report.meta.language == "en" { "Cost" } else { "Kosten" }*], [*Tokens*], [*Requests*]),
|
|
207
|
-
..report.recentPeriods.map(item => (
|
|
208
|
-
[#item.label],
|
|
209
|
-
[#item.costLabel],
|
|
210
|
-
[#item.tokensLabel],
|
|
211
|
-
[#item.requestsLabel],
|
|
212
|
-
)).flatten(),
|
|
213
|
-
)
|
|
245
|
+
]
|
|
214
246
|
]
|
|
215
247
|
|
|
216
248
|
#v(12pt)
|
|
217
249
|
|
|
218
|
-
= #
|
|
250
|
+
= #report.text.sections.interpretation
|
|
219
251
|
|
|
220
252
|
#rect(inset: 12pt, radius: 14pt, fill: panel, stroke: (paint: line, thickness: 0.8pt))[
|
|
221
|
-
#text(size: 10pt, fill: ink)[
|
|
222
|
-
#if report.meta.language == "en" [
|
|
223
|
-
This report is based on #report.meta.days daily raw entries and #report.meta.periods aggregated periods.
|
|
224
|
-
The highest-cost period is #report.labels.topDay.
|
|
225
|
-
The dominant model family is #report.labels.topModel, and the leading provider is #report.labels.topProvider.
|
|
226
|
-
] else [
|
|
227
|
-
Der Report basiert auf #report.meta.days täglichen Rohdaten und #report.meta.periods aggregierten Perioden.
|
|
228
|
-
Der kostenstärkste Zeitraum liegt bei #report.labels.topDay.
|
|
229
|
-
Die dominanteste Modellfamilie ist #report.labels.topModel, der führende Provider ist #report.labels.topProvider.
|
|
230
|
-
]
|
|
231
|
-
]
|
|
253
|
+
#text(size: 10pt, fill: ink)[#report.interpretation.summary]
|
|
232
254
|
#v(8pt)
|
|
233
|
-
#text(size: 9pt, fill: muted)[
|
|
234
|
-
#if report.meta.language == "en" [
|
|
235
|
-
Created with TTDash v#report.meta.appVersion and server-side Typst compilation.
|
|
236
|
-
] else [
|
|
237
|
-
Erstellt mit TTDash v#report.meta.appVersion und serverseitiger Typst-Kompilierung.
|
|
238
|
-
]
|
|
239
|
-
]
|
|
255
|
+
#text(size: 9pt, fill: muted)[#report.interpretation.footer]
|
|
240
256
|
]
|
|
241
257
|
`;
|
|
242
258
|
}
|
|
@@ -260,26 +276,27 @@ function createChartAssets(reportData) {
|
|
|
260
276
|
|
|
261
277
|
return {
|
|
262
278
|
'cost-trend.svg': lineChart(costTrend, {
|
|
263
|
-
title: reportData.
|
|
279
|
+
title: reportData.text.charts.costTrend,
|
|
264
280
|
valueKey: 'cost',
|
|
265
|
-
secondaryKey: reportData.meta.filterSummary.
|
|
281
|
+
secondaryKey: reportData.meta.filterSummary.viewModeKey === 'daily' ? 'ma7' : null,
|
|
266
282
|
formatter: (value) => `$${Math.round(value)}`,
|
|
267
283
|
}),
|
|
268
284
|
'top-models.svg': horizontalBarChart(topModels, {
|
|
269
|
-
title: reportData.
|
|
285
|
+
title: reportData.text.charts.topModels,
|
|
270
286
|
getValue: (entry) => entry.cost,
|
|
271
287
|
getLabel: (entry) => entry.name,
|
|
272
288
|
getColor: (entry) => entry.color,
|
|
273
289
|
formatter: (value) => `$${value.toFixed(value >= 100 ? 0 : 2)}`,
|
|
274
290
|
}),
|
|
275
291
|
'token-trend.svg': stackedBarChart(tokenTrend, {
|
|
276
|
-
title: reportData.
|
|
292
|
+
title: reportData.text.charts.tokenTrend,
|
|
293
|
+
formatter: (value) => formatCompactAxis(value, reportData.meta.language),
|
|
277
294
|
segments: [
|
|
278
|
-
{ key: 'input', label: '
|
|
279
|
-
{ key: 'output', label: '
|
|
280
|
-
{ key: 'cacheWrite', label:
|
|
281
|
-
{ key: 'cacheRead', label:
|
|
282
|
-
{ key: 'thinking', label: '
|
|
295
|
+
{ key: 'input', label: translate(reportData.meta.language, 'common.input'), color: '#0f766e' },
|
|
296
|
+
{ key: 'output', label: translate(reportData.meta.language, 'common.output'), color: '#1d4ed8' },
|
|
297
|
+
{ key: 'cacheWrite', label: translate(reportData.meta.language, 'common.cacheWrite'), color: '#b45309' },
|
|
298
|
+
{ key: 'cacheRead', label: translate(reportData.meta.language, 'common.cacheRead'), color: '#7c3aed' },
|
|
299
|
+
{ key: 'thinking', label: translate(reportData.meta.language, 'common.thinking'), color: '#be185d' },
|
|
283
300
|
],
|
|
284
301
|
}),
|
|
285
302
|
};
|
|
@@ -303,22 +320,25 @@ async function generatePdfReport(allDailyData, options = {}) {
|
|
|
303
320
|
const pdfPath = path.join(tempDir, 'report.pdf');
|
|
304
321
|
const jsonPath = path.join(tempDir, 'report.json');
|
|
305
322
|
|
|
306
|
-
|
|
307
|
-
|
|
323
|
+
try {
|
|
324
|
+
writeTextFile(typPath, buildTemplate());
|
|
325
|
+
writeTextFile(jsonPath, JSON.stringify(reportData, null, 2));
|
|
308
326
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
327
|
+
const charts = createChartAssets(reportData);
|
|
328
|
+
for (const [filename, content] of Object.entries(charts)) {
|
|
329
|
+
writeTextFile(path.join(tempDir, filename), content);
|
|
330
|
+
}
|
|
313
331
|
|
|
314
|
-
|
|
332
|
+
await compileTypst(tempDir, typPath, pdfPath);
|
|
315
333
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
334
|
+
return {
|
|
335
|
+
buffer: fs.readFileSync(pdfPath),
|
|
336
|
+
filename: `ttdash-report-${new Date().toISOString().slice(0, 10)}.pdf`,
|
|
337
|
+
reportData,
|
|
338
|
+
};
|
|
339
|
+
} finally {
|
|
340
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
341
|
+
}
|
|
322
342
|
}
|
|
323
343
|
|
|
324
344
|
module.exports = {
|