baotnd commited on
Commit
0e42d80
·
verified ·
1 Parent(s): f6c4b93

Upload folder using huggingface_hub

Browse files
trackio_custom_frontend/app.js ADDED
@@ -0,0 +1,555 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const projectSelectEl = document.querySelector("#project-select");
2
+ const runListEl = document.querySelector("#run-list");
3
+ const metricsTitleEl = document.querySelector("#metrics-title");
4
+ const metricsSubtitleEl = document.querySelector("#metrics-subtitle");
5
+ const metricsGridEl = document.querySelector("#metrics-grid");
6
+ const tracesSubtitleEl = document.querySelector("#traces-subtitle");
7
+ const tracesBodyEl = document.querySelector("#traces-body");
8
+ const navButtons = Array.from(document.querySelectorAll(".nav-link"));
9
+ const pages = Array.from(document.querySelectorAll(".page"));
10
+
11
+ const state = {
12
+ projects: [],
13
+ selectedProject: null,
14
+ runs: [],
15
+ selectedRunIds: [],
16
+ };
17
+
18
+ /*
19
+ Trackio routes used by this starter today:
20
+ - /api/get_all_projects
21
+ - /api/get_runs_for_project
22
+ - /api/get_metrics_for_run
23
+ - /api/get_metric_values
24
+ - /api/get_traces
25
+
26
+ Useful routes for expanding this starter toward the full dashboard:
27
+ - /api/get_system_metrics_for_run
28
+ - /api/get_system_logs
29
+ - /api/get_system_logs_batch
30
+ - /api/get_logs
31
+ - /api/get_logs_batch
32
+ - /api/get_snapshot
33
+ - /api/get_alerts
34
+ - /api/query_project
35
+ - /api/get_project_summary
36
+ - /api/get_run_summary
37
+ - /api/get_project_files
38
+ - /api/get_settings
39
+ - /api/get_run_mutation_status
40
+ - /api/delete_run
41
+ - /api/rename_run
42
+ - /api/force_sync
43
+ - /api/bulk_upload_media
44
+ - /api/upload
45
+
46
+ File/media URLs:
47
+ - /file?path=ABSOLUTE_PATH_FROM_API
48
+ */
49
+ const RUN_COLORS = [
50
+ "#1f77b4",
51
+ "#ff7f0e",
52
+ "#2ca02c",
53
+ "#d62728",
54
+ "#9467bd",
55
+ "#8c564b",
56
+ "#e377c2",
57
+ "#7f7f7f",
58
+ "#bcbd22",
59
+ "#17becf",
60
+ ];
61
+
62
+ async function api(name, payload = {}) {
63
+ const response = await fetch(`/api/${name}`, {
64
+ method: "POST",
65
+ headers: { "Content-Type": "application/json" },
66
+ body: JSON.stringify(payload),
67
+ });
68
+ const json = await response.json();
69
+ if (!response.ok || json.error) {
70
+ throw new Error(json.error || `Request failed for ${name}`);
71
+ }
72
+ return json.data;
73
+ }
74
+
75
+ function runKey(run) {
76
+ return run.id || run.name;
77
+ }
78
+
79
+ function colorForRun(run) {
80
+ const index = state.runs.findIndex((candidate) => runKey(candidate) === runKey(run));
81
+ return RUN_COLORS[((index >= 0 ? index : 0) % RUN_COLORS.length + RUN_COLORS.length) % RUN_COLORS.length];
82
+ }
83
+
84
+ function formatValue(value) {
85
+ if (typeof value !== "number" || !Number.isFinite(value)) {
86
+ return String(value);
87
+ }
88
+ if (Math.abs(value) >= 1000 || Math.abs(value) < 0.01) {
89
+ return value.toExponential(2);
90
+ }
91
+ return value.toFixed(3);
92
+ }
93
+
94
+ function getQueryParams() {
95
+ return new URLSearchParams(window.location.search);
96
+ }
97
+
98
+ function setQueryParams(params) {
99
+ const next = new URL(window.location.href);
100
+ for (const [key, value] of Object.entries(params)) {
101
+ if (value == null || value === "" || (Array.isArray(value) && value.length === 0)) {
102
+ next.searchParams.delete(key);
103
+ continue;
104
+ }
105
+ next.searchParams.set(key, Array.isArray(value) ? value.join(",") : value);
106
+ }
107
+ window.history.replaceState({}, "", next);
108
+ }
109
+
110
+ function setActivePage(pageName) {
111
+ navButtons.forEach((button) => {
112
+ button.classList.toggle("active", button.dataset.pageTarget === pageName);
113
+ });
114
+ pages.forEach((page) => {
115
+ page.classList.toggle("active", page.dataset.page === pageName);
116
+ });
117
+ }
118
+
119
+ function bindNavigation() {
120
+ navButtons.forEach((button) => {
121
+ button.addEventListener("click", () => setActivePage(button.dataset.pageTarget));
122
+ });
123
+ }
124
+
125
+ function pickInitialProject(projects) {
126
+ const params = getQueryParams();
127
+ const project = params.get("project");
128
+ if (project && projects.includes(project)) {
129
+ return project;
130
+ }
131
+ return projects[0] || null;
132
+ }
133
+
134
+ function pickInitialRunIds(runs) {
135
+ const params = getQueryParams();
136
+ const fromUrl = (params.get("run_ids") || "")
137
+ .split(",")
138
+ .map((item) => item.trim())
139
+ .filter(Boolean);
140
+ const validIds = runs.map(runKey);
141
+ const selected = fromUrl.filter((id) => validIds.includes(id));
142
+ if (selected.length) {
143
+ return selected;
144
+ }
145
+ return runs.slice(0, 2).map(runKey);
146
+ }
147
+
148
+ function renderProjectSelect() {
149
+ projectSelectEl.innerHTML = "";
150
+ if (!state.projects.length) {
151
+ const option = document.createElement("option");
152
+ option.value = "";
153
+ option.textContent = "No projects";
154
+ projectSelectEl.appendChild(option);
155
+ projectSelectEl.disabled = true;
156
+ return;
157
+ }
158
+
159
+ projectSelectEl.disabled = false;
160
+ for (const project of state.projects) {
161
+ const option = document.createElement("option");
162
+ option.value = project;
163
+ option.textContent = project;
164
+ option.selected = project === state.selectedProject;
165
+ projectSelectEl.appendChild(option);
166
+ }
167
+ }
168
+
169
+ function renderRunList() {
170
+ runListEl.innerHTML = "";
171
+ if (!state.runs.length) {
172
+ const empty = document.createElement("div");
173
+ empty.className = "sidebar-empty";
174
+ empty.textContent = "No runs yet";
175
+ runListEl.appendChild(empty);
176
+ return;
177
+ }
178
+
179
+ for (const run of state.runs) {
180
+ const wrapper = document.createElement("label");
181
+ wrapper.className = "run-option";
182
+
183
+ const input = document.createElement("input");
184
+ input.type = "checkbox";
185
+ input.checked = state.selectedRunIds.includes(runKey(run));
186
+ input.addEventListener("change", async () => {
187
+ if (input.checked) {
188
+ state.selectedRunIds = [...new Set([...state.selectedRunIds, runKey(run)])];
189
+ } else {
190
+ state.selectedRunIds = state.selectedRunIds.filter((id) => id !== runKey(run));
191
+ }
192
+ setQueryParams({
193
+ project: state.selectedProject,
194
+ run_ids: state.selectedRunIds,
195
+ });
196
+ await renderDashboard();
197
+ });
198
+
199
+ const marker = document.createElement("span");
200
+ marker.className = "run-color-dot";
201
+ marker.style.backgroundColor = colorForRun(run);
202
+
203
+ const text = document.createElement("span");
204
+ text.className = "run-option-text";
205
+ text.innerHTML = `<strong>${run.name || "Unnamed run"}</strong>`;
206
+
207
+ wrapper.appendChild(input);
208
+ wrapper.appendChild(marker);
209
+ wrapper.appendChild(text);
210
+ runListEl.appendChild(wrapper);
211
+ }
212
+ }
213
+
214
+ function chartPoints(rows, width, height, padding, min, max) {
215
+ const span = max - min || 1;
216
+ return rows.map((row, index) => {
217
+ const x = padding + (index / Math.max(rows.length - 1, 1)) * (width - padding * 2);
218
+ const y = height - padding - ((row.value - min) / span) * (height - padding * 2);
219
+ return [x, y];
220
+ });
221
+ }
222
+
223
+ function pathFromPoints(points) {
224
+ return points.map(([x, y], index) => `${index === 0 ? "M" : "L"} ${x} ${y}`).join(" ");
225
+ }
226
+
227
+ function renderMetricCard(metricName, seriesByRun) {
228
+ const card = document.createElement("article");
229
+ card.className = "metric-card";
230
+ const nonEmptySeries = seriesByRun.filter((entry) => entry.rows.length);
231
+ if (!nonEmptySeries.length) {
232
+ card.innerHTML = `
233
+ <div class="metric-card-head">
234
+ <div>
235
+ <h3>${metricName}</h3>
236
+ <div class="metric-run">Selected runs</div>
237
+ </div>
238
+ </div>
239
+ <div class="metric-empty">No numeric values logged for this metric.</div>
240
+ `;
241
+ return card;
242
+ }
243
+
244
+ const width = 640;
245
+ const height = 220;
246
+ const padding = 20;
247
+ const values = nonEmptySeries.flatMap((entry) => entry.rows.map((row) => row.value));
248
+ const min = Math.min(...values);
249
+ const max = Math.max(...values);
250
+ const lineMarkup = nonEmptySeries
251
+ .map((entry) => {
252
+ const points = chartPoints(entry.rows, width, height, padding, min, max);
253
+ const markers = points
254
+ .map(([x, y]) => `<circle class="plot-marker" cx="${x}" cy="${y}" r="3.5" style="stroke:${entry.color}"></circle>`)
255
+ .join("");
256
+ return `
257
+ <path class="plot-line" d="${pathFromPoints(points)}" stroke="${entry.color}"></path>
258
+ ${markers}
259
+ `;
260
+ })
261
+ .join("");
262
+ const legendMarkup = nonEmptySeries
263
+ .map(
264
+ (entry) => `
265
+ <span class="metric-legend-item">
266
+ <span class="metric-legend-dot" style="background:${entry.color}"></span>
267
+ ${entry.runName}
268
+ </span>
269
+ `,
270
+ )
271
+ .join("");
272
+ const latestSummary = nonEmptySeries
273
+ .map((entry) => `${entry.runName}: ${formatValue(entry.rows.at(-1).value)}`)
274
+ .join(" | ");
275
+
276
+ card.innerHTML = `
277
+ <div class="metric-card-head">
278
+ <div>
279
+ <h3>${metricName}</h3>
280
+ <div class="metric-run">${nonEmptySeries.length} run${nonEmptySeries.length === 1 ? "" : "s"} overlaid</div>
281
+ </div>
282
+ <div class="metric-latest">${latestSummary}</div>
283
+ </div>
284
+ <div class="plot-shell">
285
+ <svg viewBox="0 0 ${width} ${height}" role="img" aria-label="${metricName} line plot">
286
+ <line class="plot-axis" x1="${padding}" y1="${height - padding}" x2="${width - padding}" y2="${height - padding}"></line>
287
+ ${lineMarkup}
288
+ </svg>
289
+ </div>
290
+ <div class="metric-legend">${legendMarkup}</div>
291
+ <div class="metric-meta">Comparing ${nonEmptySeries.length} selected runs on the same metric scale.</div>
292
+ `;
293
+ return card;
294
+ }
295
+
296
+ function textFromContent(content) {
297
+ if (typeof content === "string") return content;
298
+ if (Array.isArray(content)) {
299
+ return content
300
+ .map((part) => {
301
+ if (typeof part === "string") return part;
302
+ if (typeof part?.text === "string") return part.text;
303
+ if (typeof part?.content === "string") return part.content;
304
+ return "";
305
+ })
306
+ .filter(Boolean)
307
+ .join(" ");
308
+ }
309
+ if (typeof content?.text === "string") return content.text;
310
+ return "";
311
+ }
312
+
313
+ function escapeHtml(value) {
314
+ return String(value)
315
+ .replaceAll("&", "&amp;")
316
+ .replaceAll("<", "&lt;")
317
+ .replaceAll(">", "&gt;")
318
+ .replaceAll('"', "&quot;")
319
+ .replaceAll("'", "&#39;");
320
+ }
321
+
322
+ function renderMessageContent(content) {
323
+ if (typeof content === "string") {
324
+ return `<div class="trace-message-text">${escapeHtml(content)}</div>`;
325
+ }
326
+ if (Array.isArray(content)) {
327
+ const items = content
328
+ .map((part) => {
329
+ if (typeof part === "string") {
330
+ return `<div class="trace-message-text">${escapeHtml(part)}</div>`;
331
+ }
332
+ if (typeof part?.text === "string") {
333
+ return `<div class="trace-message-text">${escapeHtml(part.text)}</div>`;
334
+ }
335
+ if (typeof part?.content === "string") {
336
+ return `<div class="trace-message-text">${escapeHtml(part.content)}</div>`;
337
+ }
338
+ return `<div class="trace-message-text trace-message-muted">[non-text content]</div>`;
339
+ })
340
+ .join("");
341
+ return items || '<div class="trace-message-text trace-message-muted">(empty)</div>';
342
+ }
343
+ if (typeof content?.text === "string") {
344
+ return `<div class="trace-message-text">${escapeHtml(content.text)}</div>`;
345
+ }
346
+ return '<div class="trace-message-text trace-message-muted">(empty)</div>';
347
+ }
348
+
349
+ function renderTraceDetail(trace) {
350
+ const messages = Array.isArray(trace.messages) ? trace.messages : [];
351
+ if (!messages.length) {
352
+ return '<div class="trace-message-text trace-message-muted">No trace messages.</div>';
353
+ }
354
+ return messages
355
+ .map((message) => {
356
+ const role = escapeHtml(message?.role || "unknown");
357
+ return `
358
+ <div class="trace-message">
359
+ <div class="trace-message-role">${role}</div>
360
+ ${renderMessageContent(message?.content)}
361
+ </div>
362
+ `;
363
+ })
364
+ .join("");
365
+ }
366
+
367
+ function formatTraceTime(timestamp) {
368
+ if (!timestamp) return "—";
369
+ const date = new Date(timestamp);
370
+ if (Number.isNaN(date.getTime())) {
371
+ return timestamp;
372
+ }
373
+ return date.toLocaleString();
374
+ }
375
+
376
+ function renderTraceRows(traces) {
377
+ tracesBodyEl.innerHTML = "";
378
+ if (!traces.length) {
379
+ const row = document.createElement("tr");
380
+ row.innerHTML = '<td colspan="5" class="empty-row">No traces for the selected runs.</td>';
381
+ tracesBodyEl.appendChild(row);
382
+ return;
383
+ }
384
+
385
+ for (const trace of traces) {
386
+ const request = textFromContent(
387
+ (trace.messages || []).find((message) => message?.role === "user")?.content,
388
+ ) || "(no user message)";
389
+ const row = document.createElement("tr");
390
+ row.className = "trace-summary-row";
391
+ row.setAttribute("role", "button");
392
+ row.setAttribute("tabindex", "0");
393
+ row.setAttribute("aria-expanded", "false");
394
+ row.innerHTML = `
395
+ <td><span class="trace-id">${trace.id}</span></td>
396
+ <td class="trace-request">${request}</td>
397
+ <td>${trace.run || "—"}</td>
398
+ <td>${trace.step ?? "—"}</td>
399
+ <td>${formatTraceTime(trace.timestamp)}</td>
400
+ `;
401
+ const detailRow = document.createElement("tr");
402
+ detailRow.className = "trace-detail-row";
403
+ detailRow.hidden = true;
404
+ detailRow.innerHTML = `
405
+ <td colspan="5">
406
+ <div class="trace-detail-shell">
407
+ <div class="trace-detail-head">
408
+ <div>
409
+ <strong>${escapeHtml(trace.id)}</strong>
410
+ <div class="trace-detail-meta">${escapeHtml(trace.run || "—")} | step ${escapeHtml(trace.step ?? "—")} | ${escapeHtml(formatTraceTime(trace.timestamp))}</div>
411
+ </div>
412
+ </div>
413
+ <div class="trace-message-list">
414
+ ${renderTraceDetail(trace)}
415
+ </div>
416
+ </div>
417
+ </td>
418
+ `;
419
+ const toggleRow = () => {
420
+ const expanded = row.getAttribute("aria-expanded") === "true";
421
+ row.setAttribute("aria-expanded", expanded ? "false" : "true");
422
+ row.classList.toggle("expanded", !expanded);
423
+ detailRow.hidden = expanded;
424
+ };
425
+ row.addEventListener("click", toggleRow);
426
+ row.addEventListener("keydown", (event) => {
427
+ if (event.key === "Enter" || event.key === " ") {
428
+ event.preventDefault();
429
+ toggleRow();
430
+ }
431
+ });
432
+ tracesBodyEl.appendChild(row);
433
+ tracesBodyEl.appendChild(detailRow);
434
+ }
435
+ }
436
+
437
+ async function loadRuns() {
438
+ if (!state.selectedProject) {
439
+ state.runs = [];
440
+ state.selectedRunIds = [];
441
+ renderRunList();
442
+ await renderDashboard();
443
+ return;
444
+ }
445
+
446
+ state.runs = await api("get_runs_for_project", { project: state.selectedProject });
447
+ state.selectedRunIds = pickInitialRunIds(state.runs);
448
+ renderRunList();
449
+ await renderDashboard();
450
+ }
451
+
452
+ async function renderDashboard() {
453
+ metricsGridEl.innerHTML = "";
454
+ tracesBodyEl.innerHTML = "";
455
+
456
+ const selectedRuns = state.runs.filter((run) => state.selectedRunIds.includes(runKey(run)));
457
+ metricsTitleEl.textContent = state.selectedProject || "Metrics";
458
+
459
+ if (!state.selectedProject) {
460
+ metricsSubtitleEl.textContent = "No Trackio projects found.";
461
+ tracesSubtitleEl.textContent = "No traces available.";
462
+ return;
463
+ }
464
+
465
+ if (!selectedRuns.length) {
466
+ metricsSubtitleEl.textContent = "Select one or more runs in the sidebar.";
467
+ tracesSubtitleEl.textContent = "Select one or more runs to load traces.";
468
+ metricsGridEl.innerHTML = '<div class="empty-panel">No runs selected.</div>';
469
+ renderTraceRows([]);
470
+ return;
471
+ }
472
+
473
+ metricsSubtitleEl.textContent = `Plot cards for ${selectedRuns.length} selected run${selectedRuns.length === 1 ? "" : "s"}.`;
474
+ tracesSubtitleEl.textContent = `Recent traces for ${selectedRuns.length} selected run${selectedRuns.length === 1 ? "" : "s"}.`;
475
+
476
+ const traceGroups = [];
477
+ const metricMap = new Map();
478
+
479
+ for (const run of selectedRuns) {
480
+ const metrics = await api("get_metrics_for_run", {
481
+ project: state.selectedProject,
482
+ run: run.name,
483
+ run_id: run.id,
484
+ });
485
+
486
+ const metricSeries = await Promise.all(
487
+ metrics.slice(0, 3).map(async (metricName) => ({
488
+ metricName,
489
+ rows: await api("get_metric_values", {
490
+ project: state.selectedProject,
491
+ run: run.name,
492
+ run_id: run.id,
493
+ metric_name: metricName,
494
+ }),
495
+ })),
496
+ );
497
+
498
+ metricSeries.forEach(({ metricName, rows }) => {
499
+ const numericRows = rows.filter((row) => typeof row.value === "number" && Number.isFinite(row.value));
500
+ if (!metricMap.has(metricName)) {
501
+ metricMap.set(metricName, []);
502
+ }
503
+ metricMap.get(metricName).push({
504
+ runName: run.name || "Unnamed run",
505
+ color: colorForRun(run),
506
+ rows: numericRows,
507
+ });
508
+ });
509
+
510
+ const runTraces = await api("get_traces", {
511
+ project: state.selectedProject,
512
+ run: run.name,
513
+ run_id: run.id,
514
+ sort: "request_time_desc",
515
+ limit: 6,
516
+ });
517
+ traceGroups.push(...runTraces);
518
+ }
519
+
520
+ for (const [metricName, seriesByRun] of metricMap.entries()) {
521
+ metricsGridEl.appendChild(renderMetricCard(metricName, seriesByRun));
522
+ }
523
+
524
+ if (!metricsGridEl.children.length) {
525
+ metricsGridEl.innerHTML = '<div class="empty-panel">No numeric metrics available.</div>';
526
+ }
527
+
528
+ traceGroups.sort((left, right) => String(right.timestamp || "").localeCompare(String(left.timestamp || "")));
529
+ renderTraceRows(traceGroups.slice(0, 12));
530
+ }
531
+
532
+ async function load() {
533
+ bindNavigation();
534
+ projectSelectEl.addEventListener("change", async () => {
535
+ state.selectedProject = projectSelectEl.value || null;
536
+ setQueryParams({ project: state.selectedProject, run_ids: null });
537
+ await loadRuns();
538
+ renderProjectSelect();
539
+ });
540
+ try {
541
+ state.projects = await api("get_all_projects");
542
+ state.selectedProject = pickInitialProject(state.projects);
543
+ renderProjectSelect();
544
+ await loadRuns();
545
+ } catch (error) {
546
+ projectSelectEl.innerHTML = '<option value="">Error</option>';
547
+ projectSelectEl.disabled = true;
548
+ metricsSubtitleEl.textContent = "Could not load Trackio data.";
549
+ metricsGridEl.innerHTML = '<div class="empty-panel">The starter could not reach the Trackio API.</div>';
550
+ tracesSubtitleEl.textContent = "Could not load traces.";
551
+ renderTraceRows([]);
552
+ }
553
+ }
554
+
555
+ load();
trackio_custom_frontend/index.html ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Starter</title>
7
+ <link rel="stylesheet" href="./styles.css" />
8
+ </head>
9
+ <body>
10
+ <div class="app-shell">
11
+ <aside class="sidebar">
12
+ <div class="sidebar-scroll">
13
+ <div class="logo-section">
14
+ <img
15
+ src="/static/trackio/trackio_logo_type_light_transparent.png"
16
+ alt="Trackio"
17
+ class="logo"
18
+ />
19
+ </div>
20
+
21
+ <section class="sidebar-section">
22
+ <div class="section-label">Project</div>
23
+ <div class="dropdown-wrap">
24
+ <select id="project-select" class="project-select" aria-label="Project"></select>
25
+ <div class="dropdown-icon" aria-hidden="true">
26
+ <svg width="16" height="16" viewBox="0 0 18 18" fill="none">
27
+ <path d="M5.25 7.5L9 11.25L12.75 7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
28
+ </svg>
29
+ </div>
30
+ </div>
31
+ </section>
32
+
33
+ <section class="sidebar-section">
34
+ <div class="section-label">Runs</div>
35
+ <div id="run-list" class="run-list"></div>
36
+ </section>
37
+
38
+ <section class="sidebar-section">
39
+ <div class="section-label">Notes</div>
40
+ <p class="sidebar-note">
41
+ This starter mirrors the Trackio dashboard structure, but stays plain
42
+ HTML, CSS, and JavaScript so you can replace pieces quickly.
43
+ </p>
44
+ </section>
45
+ </div>
46
+ </aside>
47
+
48
+ <main class="main-shell">
49
+ <nav class="navbar">
50
+ <div class="nav-spacer"></div>
51
+ <div class="nav-tabs">
52
+ <button class="nav-link active" data-page-target="metrics">Metrics</button>
53
+ <button class="nav-link" data-page-target="traces">Traces</button>
54
+ <!--
55
+ Trackio's full dashboard also includes tabs like these.
56
+ Uncomment them when you implement the corresponding page sections.
57
+
58
+ <button class="nav-link" data-page-target="system">System Metrics</button>
59
+ <button class="nav-link" data-page-target="media">Media & Tables</button>
60
+ <button class="nav-link" data-page-target="reports">Alerts & Reports</button>
61
+ <button class="nav-link" data-page-target="runs">Runs</button>
62
+ <button class="nav-link" data-page-target="files">Files</button>
63
+ <button class="nav-link" data-page-target="settings">Settings</button>
64
+ -->
65
+ </div>
66
+ </nav>
67
+
68
+ <section class="page active" data-page="metrics">
69
+ <header class="page-header">
70
+ <div>
71
+ <p class="eyebrow">Starter Dashboard</p>
72
+ <h1 id="metrics-title">Metrics</h1>
73
+ <p id="metrics-subtitle" class="page-subtitle">Loading Trackio data.</p>
74
+ </div>
75
+ </header>
76
+ <div id="metrics-grid" class="metrics-grid"></div>
77
+ </section>
78
+
79
+ <section class="page" data-page="traces">
80
+ <header class="page-header">
81
+ <div>
82
+ <p class="eyebrow">Starter Dashboard</p>
83
+ <h1>Traces</h1>
84
+ <p id="traces-subtitle" class="page-subtitle">Recent traces for the selected runs.</p>
85
+ </div>
86
+ </header>
87
+ <div class="traces-table-wrap">
88
+ <table class="traces-table">
89
+ <thead>
90
+ <tr>
91
+ <th>Trace ID</th>
92
+ <th>Request</th>
93
+ <th>Run</th>
94
+ <th>Step</th>
95
+ <th>Request time</th>
96
+ </tr>
97
+ </thead>
98
+ <tbody id="traces-body"></tbody>
99
+ </table>
100
+ </div>
101
+ </section>
102
+
103
+ <!--
104
+ Future page shells you may want to add:
105
+
106
+ <section class="page" data-page="system">
107
+ Build this from /api/get_system_metrics_for_run and /api/get_system_logs.
108
+ </section>
109
+
110
+ <section class="page" data-page="media">
111
+ Build this from /api/get_logs, /api/get_snapshot, /api/get_project_files, and /file?path=...
112
+ </section>
113
+
114
+ <section class="page" data-page="reports">
115
+ Build this from /api/get_alerts and /api/query_project.
116
+ </section>
117
+
118
+ <section class="page" data-page="runs">
119
+ Build this from /api/get_runs_for_project, /api/get_run_summary, /api/delete_run, and /api/rename_run.
120
+ </section>
121
+
122
+ <section class="page" data-page="files">
123
+ Build this from /api/get_project_files and /file?path=...
124
+ </section>
125
+
126
+ <section class="page" data-page="settings">
127
+ Build this from /api/get_settings, /api/force_sync, and /api/get_run_mutation_status.
128
+ </section>
129
+ -->
130
+ </main>
131
+ </div>
132
+
133
+ <script type="module" src="./app.js"></script>
134
+ </body>
135
+ </html>
trackio_custom_frontend/styles.css ADDED
@@ -0,0 +1,467 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ color-scheme: light;
3
+ --background-fill-primary: #ffffff;
4
+ --background-fill-secondary: #f9fafb;
5
+ --background-fill-tertiary: #f3f4f6;
6
+ --border-color-primary: #e5e7eb;
7
+ --border-color-accent: #d1d5db;
8
+ --body-text-color: #111827;
9
+ --body-text-color-subdued: #6b7280;
10
+ --body-text-color-soft: #9ca3af;
11
+ --accent: #1d4ed8;
12
+ --shadow-soft: 0 1px 2px rgba(16, 24, 40, 0.04);
13
+ }
14
+
15
+ * {
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ html,
20
+ body {
21
+ margin: 0;
22
+ min-height: 100%;
23
+ background: var(--background-fill-secondary);
24
+ color: var(--body-text-color);
25
+ font-family:
26
+ ui-sans-serif,
27
+ system-ui,
28
+ -apple-system,
29
+ BlinkMacSystemFont,
30
+ "Segoe UI",
31
+ sans-serif;
32
+ }
33
+
34
+ button,
35
+ input,
36
+ select,
37
+ table {
38
+ font: inherit;
39
+ }
40
+
41
+ .app-shell {
42
+ display: grid;
43
+ grid-template-columns: 320px minmax(0, 1fr);
44
+ min-height: 100vh;
45
+ }
46
+
47
+ .sidebar {
48
+ border-right: 1px solid var(--border-color-primary);
49
+ background: var(--background-fill-primary);
50
+ }
51
+
52
+ .sidebar-scroll {
53
+ height: 100vh;
54
+ overflow-y: auto;
55
+ padding: 18px 16px 28px;
56
+ }
57
+
58
+ .logo-section {
59
+ padding: 10px 10px 18px;
60
+ border-bottom: 1px solid var(--border-color-primary);
61
+ }
62
+
63
+ .logo {
64
+ display: block;
65
+ width: 138px;
66
+ max-width: 100%;
67
+ }
68
+
69
+ .sidebar-section {
70
+ padding: 18px 10px 0;
71
+ }
72
+
73
+ .section-label,
74
+ .eyebrow {
75
+ margin: 0 0 10px;
76
+ color: var(--body-text-color-subdued);
77
+ font-size: 12px;
78
+ font-weight: 600;
79
+ letter-spacing: 0.08em;
80
+ text-transform: uppercase;
81
+ }
82
+
83
+ .run-list {
84
+ display: grid;
85
+ gap: 8px;
86
+ }
87
+
88
+ .dropdown-wrap {
89
+ position: relative;
90
+ }
91
+
92
+ .project-select {
93
+ width: 100%;
94
+ padding: 10px 40px 10px 12px;
95
+ border: 1px solid var(--border-color-primary);
96
+ border-radius: 10px;
97
+ background: var(--background-fill-primary);
98
+ color: var(--body-text-color);
99
+ appearance: none;
100
+ -webkit-appearance: none;
101
+ }
102
+
103
+ .project-select:focus {
104
+ outline: none;
105
+ border-color: var(--border-color-accent);
106
+ box-shadow: 0 0 0 3px rgba(29, 78, 216, 0.08);
107
+ }
108
+
109
+ .dropdown-icon {
110
+ position: absolute;
111
+ top: 50%;
112
+ right: 12px;
113
+ transform: translateY(-50%);
114
+ color: var(--body-text-color-subdued);
115
+ pointer-events: none;
116
+ }
117
+
118
+ .run-option {
119
+ display: grid;
120
+ grid-template-columns: 18px 10px minmax(0, 1fr);
121
+ gap: 10px;
122
+ align-items: start;
123
+ padding: 10px 12px;
124
+ border: 1px solid var(--border-color-primary);
125
+ border-radius: 10px;
126
+ background: var(--background-fill-primary);
127
+ cursor: pointer;
128
+ }
129
+
130
+ .run-option input {
131
+ margin: 2px 0 0;
132
+ accent-color: var(--accent);
133
+ }
134
+
135
+ .run-color-dot {
136
+ width: 10px;
137
+ height: 10px;
138
+ margin-top: 5px;
139
+ border-radius: 999px;
140
+ background: var(--accent);
141
+ }
142
+
143
+ .run-option-text strong,
144
+ .run-option-text span {
145
+ display: block;
146
+ }
147
+
148
+ .run-option-text strong {
149
+ color: var(--body-text-color);
150
+ font-size: 14px;
151
+ font-weight: 600;
152
+ }
153
+
154
+ .run-option-text span,
155
+ .sidebar-note,
156
+ .sidebar-empty {
157
+ color: var(--body-text-color-subdued);
158
+ font-size: 13px;
159
+ line-height: 1.5;
160
+ }
161
+
162
+ .main-shell {
163
+ display: flex;
164
+ flex-direction: column;
165
+ min-width: 0;
166
+ }
167
+
168
+ .navbar {
169
+ display: flex;
170
+ align-items: stretch;
171
+ min-height: 44px;
172
+ border-bottom: 1px solid var(--border-color-primary);
173
+ background: var(--background-fill-primary);
174
+ }
175
+
176
+ .nav-spacer {
177
+ flex: 1 1 0;
178
+ }
179
+
180
+ .nav-tabs {
181
+ display: flex;
182
+ padding-right: 8px;
183
+ }
184
+
185
+ .nav-link {
186
+ padding: 10px 16px;
187
+ border: none;
188
+ border-bottom: 2px solid transparent;
189
+ background: none;
190
+ color: var(--body-text-color-subdued);
191
+ cursor: pointer;
192
+ }
193
+
194
+ .nav-link.active {
195
+ border-bottom-color: var(--body-text-color);
196
+ color: var(--body-text-color);
197
+ font-weight: 500;
198
+ }
199
+
200
+ .page {
201
+ display: none;
202
+ min-width: 0;
203
+ padding: 24px 28px 36px;
204
+ }
205
+
206
+ .page.active {
207
+ display: block;
208
+ }
209
+
210
+ .page-header {
211
+ margin-bottom: 22px;
212
+ }
213
+
214
+ .page-header h1 {
215
+ margin: 0;
216
+ font-size: 32px;
217
+ line-height: 1.1;
218
+ }
219
+
220
+ .page-subtitle {
221
+ margin: 8px 0 0;
222
+ color: var(--body-text-color-subdued);
223
+ font-size: 15px;
224
+ }
225
+
226
+ .metrics-grid {
227
+ display: grid;
228
+ gap: 18px;
229
+ }
230
+
231
+ .metric-card {
232
+ padding: 18px;
233
+ border: 1px solid var(--border-color-primary);
234
+ border-radius: 14px;
235
+ background: var(--background-fill-primary);
236
+ box-shadow: var(--shadow-soft);
237
+ }
238
+
239
+ .metric-card-head {
240
+ display: flex;
241
+ align-items: start;
242
+ justify-content: space-between;
243
+ gap: 16px;
244
+ }
245
+
246
+ .metric-card h3 {
247
+ margin: 0;
248
+ font-size: 18px;
249
+ }
250
+
251
+ .metric-run,
252
+ .metric-meta,
253
+ .metric-empty,
254
+ .metric-latest {
255
+ color: var(--body-text-color-subdued);
256
+ font-size: 13px;
257
+ }
258
+
259
+ .metric-latest {
260
+ color: var(--body-text-color);
261
+ max-width: 50%;
262
+ font-size: 13px;
263
+ font-weight: 600;
264
+ text-align: right;
265
+ }
266
+
267
+ .plot-shell {
268
+ margin-top: 14px;
269
+ padding: 10px 12px;
270
+ border: 1px solid var(--border-color-primary);
271
+ border-radius: 12px;
272
+ background: linear-gradient(180deg, #ffffff, #f9fafb);
273
+ }
274
+
275
+ .plot-shell svg {
276
+ display: block;
277
+ width: 100%;
278
+ height: auto;
279
+ }
280
+
281
+ .plot-axis {
282
+ stroke: var(--border-color-accent);
283
+ stroke-width: 1.2;
284
+ }
285
+
286
+ .plot-line {
287
+ fill: none;
288
+ stroke-width: 2.25;
289
+ stroke-linecap: round;
290
+ stroke-linejoin: round;
291
+ }
292
+
293
+ .plot-marker {
294
+ fill: var(--background-fill-primary);
295
+ stroke: var(--body-text-color);
296
+ stroke-width: 1.5;
297
+ }
298
+
299
+ .metric-legend {
300
+ display: flex;
301
+ flex-wrap: wrap;
302
+ gap: 10px 14px;
303
+ margin-top: 12px;
304
+ }
305
+
306
+ .metric-legend-item {
307
+ display: inline-flex;
308
+ align-items: center;
309
+ gap: 8px;
310
+ color: var(--body-text-color-subdued);
311
+ font-size: 13px;
312
+ }
313
+
314
+ .metric-legend-dot {
315
+ width: 10px;
316
+ height: 10px;
317
+ border-radius: 999px;
318
+ }
319
+
320
+ .traces-table-wrap {
321
+ overflow: auto;
322
+ border: 1px solid var(--border-color-primary);
323
+ border-radius: 14px;
324
+ background: var(--background-fill-primary);
325
+ box-shadow: var(--shadow-soft);
326
+ }
327
+
328
+ .traces-table {
329
+ width: 100%;
330
+ border-collapse: collapse;
331
+ }
332
+
333
+ .traces-table thead {
334
+ background: var(--background-fill-secondary);
335
+ }
336
+
337
+ .traces-table th,
338
+ .traces-table td {
339
+ padding: 14px 16px;
340
+ border-bottom: 1px solid var(--border-color-primary);
341
+ text-align: left;
342
+ vertical-align: top;
343
+ font-size: 14px;
344
+ }
345
+
346
+ .traces-table th {
347
+ color: var(--body-text-color-subdued);
348
+ font-size: 12px;
349
+ font-weight: 600;
350
+ letter-spacing: 0.04em;
351
+ text-transform: uppercase;
352
+ }
353
+
354
+ .trace-summary-row {
355
+ cursor: pointer;
356
+ }
357
+
358
+ .trace-summary-row:hover {
359
+ background: var(--background-fill-secondary);
360
+ }
361
+
362
+ .trace-summary-row.expanded {
363
+ background: var(--background-fill-secondary);
364
+ }
365
+
366
+ .trace-id {
367
+ color: var(--body-text-color);
368
+ font-family:
369
+ ui-monospace,
370
+ SFMono-Regular,
371
+ Menlo,
372
+ monospace;
373
+ font-size: 12px;
374
+ }
375
+
376
+ .trace-request {
377
+ max-width: 520px;
378
+ color: var(--body-text-color);
379
+ }
380
+
381
+ .trace-detail-row td {
382
+ padding: 0;
383
+ background: var(--background-fill-secondary);
384
+ }
385
+
386
+ .trace-detail-shell {
387
+ padding: 18px 20px;
388
+ border-top: 1px solid var(--border-color-primary);
389
+ }
390
+
391
+ .trace-detail-head strong {
392
+ display: block;
393
+ color: var(--body-text-color);
394
+ font-size: 14px;
395
+ }
396
+
397
+ .trace-detail-meta {
398
+ margin-top: 4px;
399
+ color: var(--body-text-color-subdued);
400
+ font-size: 12px;
401
+ }
402
+
403
+ .trace-message-list {
404
+ display: grid;
405
+ gap: 12px;
406
+ margin-top: 16px;
407
+ }
408
+
409
+ .trace-message {
410
+ padding: 12px 14px;
411
+ border: 1px solid var(--border-color-primary);
412
+ border-radius: 12px;
413
+ background: var(--background-fill-primary);
414
+ }
415
+
416
+ .trace-message-role {
417
+ margin-bottom: 8px;
418
+ color: var(--body-text-color-subdued);
419
+ font-size: 12px;
420
+ font-weight: 600;
421
+ letter-spacing: 0.04em;
422
+ text-transform: uppercase;
423
+ }
424
+
425
+ .trace-message-text {
426
+ color: var(--body-text-color);
427
+ font-size: 14px;
428
+ line-height: 1.55;
429
+ white-space: pre-wrap;
430
+ overflow-wrap: anywhere;
431
+ }
432
+
433
+ .trace-message-muted {
434
+ color: var(--body-text-color-subdued);
435
+ }
436
+
437
+ .empty-row,
438
+ .empty-panel {
439
+ color: var(--body-text-color-subdued);
440
+ text-align: center;
441
+ }
442
+
443
+ .empty-panel {
444
+ padding: 48px 20px;
445
+ border: 1px dashed var(--border-color-accent);
446
+ border-radius: 14px;
447
+ background: var(--background-fill-primary);
448
+ }
449
+
450
+ @media (max-width: 960px) {
451
+ .app-shell {
452
+ grid-template-columns: 1fr;
453
+ }
454
+
455
+ .sidebar {
456
+ border-right: none;
457
+ border-bottom: 1px solid var(--border-color-primary);
458
+ }
459
+
460
+ .sidebar-scroll {
461
+ height: auto;
462
+ }
463
+
464
+ .page {
465
+ padding: 18px;
466
+ }
467
+ }