@roastcodes/ttdash 6.1.4 → 6.1.6
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 +14 -4
- package/dist/assets/AutoImportModal-Dqbl8H04.js +2 -0
- package/dist/assets/CustomTooltip-DBPq6A_5.js +1 -0
- package/dist/assets/DrillDownModal-BKuxN4GY.js +1 -0
- package/dist/assets/index-D8uaHhGW.js +4 -0
- package/dist/assets/index-TppJ6Iqj.css +2 -0
- package/dist/index.html +4 -4
- package/package.json +18 -5
- package/server/model-normalization.json +28 -0
- package/server/report/charts.js +151 -64
- package/server/report/index.js +149 -109
- package/server/report/utils.js +482 -85
- package/server.js +305 -191
- package/src/locales/de/common.json +78 -1
- package/src/locales/en/common.json +78 -1
- package/usage-normalizer.js +44 -36
- package/dist/assets/AutoImportModal-Dig6ASar.js +0 -2
- package/dist/assets/CustomTooltip-YeXs5zcp.js +0 -1
- package/dist/assets/DrillDownModal-Ct7hxDzy.js +0 -1
- package/dist/assets/index-9cPAel40.js +0 -4
- package/dist/assets/index-DWoj-vpZ.css +0 -2
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,47 @@ 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
|
-
{
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
295
|
+
{
|
|
296
|
+
key: 'input',
|
|
297
|
+
label: translate(reportData.meta.language, 'common.input'),
|
|
298
|
+
color: '#0f766e',
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
key: 'output',
|
|
302
|
+
label: translate(reportData.meta.language, 'common.output'),
|
|
303
|
+
color: '#1d4ed8',
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
key: 'cacheWrite',
|
|
307
|
+
label: translate(reportData.meta.language, 'common.cacheWrite'),
|
|
308
|
+
color: '#b45309',
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
key: 'cacheRead',
|
|
312
|
+
label: translate(reportData.meta.language, 'common.cacheRead'),
|
|
313
|
+
color: '#7c3aed',
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
key: 'thinking',
|
|
317
|
+
label: translate(reportData.meta.language, 'common.thinking'),
|
|
318
|
+
color: '#be185d',
|
|
319
|
+
},
|
|
283
320
|
],
|
|
284
321
|
}),
|
|
285
322
|
};
|
|
@@ -303,22 +340,25 @@ async function generatePdfReport(allDailyData, options = {}) {
|
|
|
303
340
|
const pdfPath = path.join(tempDir, 'report.pdf');
|
|
304
341
|
const jsonPath = path.join(tempDir, 'report.json');
|
|
305
342
|
|
|
306
|
-
|
|
307
|
-
|
|
343
|
+
try {
|
|
344
|
+
writeTextFile(typPath, buildTemplate());
|
|
345
|
+
writeTextFile(jsonPath, JSON.stringify(reportData, null, 2));
|
|
308
346
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
347
|
+
const charts = createChartAssets(reportData);
|
|
348
|
+
for (const [filename, content] of Object.entries(charts)) {
|
|
349
|
+
writeTextFile(path.join(tempDir, filename), content);
|
|
350
|
+
}
|
|
313
351
|
|
|
314
|
-
|
|
352
|
+
await compileTypst(tempDir, typPath, pdfPath);
|
|
315
353
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
354
|
+
return {
|
|
355
|
+
buffer: fs.readFileSync(pdfPath),
|
|
356
|
+
filename: `ttdash-report-${new Date().toISOString().slice(0, 10)}.pdf`,
|
|
357
|
+
reportData,
|
|
358
|
+
};
|
|
359
|
+
} finally {
|
|
360
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
361
|
+
}
|
|
322
362
|
}
|
|
323
363
|
|
|
324
364
|
module.exports = {
|