302 lines
12 KiB
HTML
302 lines
12 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block content %}
|
|
<div class="level">
|
|
<div class="level-left">
|
|
<div class="level-item">
|
|
<div>
|
|
<h1 class="title is-4">Traces</h1>
|
|
<p class="subtitle is-6">Tracked model calls and usage metrics for this account.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="level-right">
|
|
<div class="level-item">
|
|
{% if stats.total_runs %}
|
|
<span class="tag is-success is-light">Tracking Active</span>
|
|
{% else %}
|
|
<span class="tag is-warning is-light">No Runs Yet</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<article class="notification is-light">
|
|
<p class="is-size-7 has-text-grey-dark">Execution health at a glance</p>
|
|
<div class="tags mt-2">
|
|
<span class="tag is-light">Total {{ stats.total_runs }}</span>
|
|
<span class="tag is-success is-light">OK {{ stats.total_ok }}</span>
|
|
<span class="tag is-danger is-light">Failed {{ stats.total_failed }}</span>
|
|
<span class="tag is-info is-light">24h {{ stats.last_24h_runs }}</span>
|
|
<span class="tag is-warning is-light">24h Failed {{ stats.last_24h_failed }}</span>
|
|
<span class="tag is-link is-light">7d {{ stats.last_7d_runs }}</span>
|
|
</div>
|
|
<p class="is-size-7 has-text-grey-dark mt-3">Success Rate</p>
|
|
<progress class="progress is-link is-small" value="{{ stats.success_rate }}" max="100">{{ stats.success_rate }}%</progress>
|
|
</article>
|
|
|
|
<div class="columns is-multiline">
|
|
<div class="column is-12-tablet is-4-desktop">
|
|
<article class="card">
|
|
<header class="card-header">
|
|
<p class="card-header-title is-size-6">Reliability</p>
|
|
</header>
|
|
<div class="card-content">
|
|
<table class="table is-fullwidth is-narrow is-size-7">
|
|
<tbody>
|
|
<tr><th>Total Runs</th><td>{{ stats.total_runs }}</td></tr>
|
|
<tr><th>OK</th><td class="has-text-success">{{ stats.total_ok }}</td></tr>
|
|
<tr><th>Failed</th><td class="has-text-danger">{{ stats.total_failed }}</td></tr>
|
|
<tr><th>Success Rate</th><td>{{ stats.success_rate }}%</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
<div class="column is-12-tablet is-4-desktop">
|
|
<article class="card">
|
|
<header class="card-header">
|
|
<p class="card-header-title is-size-6">Throughput</p>
|
|
</header>
|
|
<div class="card-content">
|
|
<table class="table is-fullwidth is-narrow is-size-7">
|
|
<tbody>
|
|
<tr><th>Runs (24h)</th><td>{{ stats.last_24h_runs }}</td></tr>
|
|
<tr><th>Failed (24h)</th><td>{{ stats.last_24h_failed }}</td></tr>
|
|
<tr><th>Runs (7d)</th><td>{{ stats.last_7d_runs }}</td></tr>
|
|
<tr><th>Avg Duration</th><td>{{ stats.avg_duration_ms }}ms</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
<div class="column is-12-tablet is-4-desktop">
|
|
<article class="card">
|
|
<header class="card-header">
|
|
<p class="card-header-title is-size-6">Token Proxy (Chars)</p>
|
|
</header>
|
|
<div class="card-content">
|
|
<table class="table is-fullwidth is-narrow is-size-7">
|
|
<tbody>
|
|
<tr><th>Total Prompt</th><td>{{ stats.total_prompt_chars }}</td></tr>
|
|
<tr><th>Total Response</th><td>{{ stats.total_response_chars }}</td></tr>
|
|
<tr><th>Avg Prompt</th><td>{{ stats.avg_prompt_chars }}</td></tr>
|
|
<tr><th>Avg Response</th><td>{{ stats.avg_response_chars }}</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="columns">
|
|
<div class="column is-6">
|
|
<article class="card">
|
|
<header class="card-header">
|
|
<p class="card-header-title is-size-6">By Operation</p>
|
|
</header>
|
|
<div class="card-content">
|
|
<div class="table-container">
|
|
<table class="table is-fullwidth is-size-7 is-striped is-hoverable">
|
|
<thead>
|
|
<tr><th>Operation</th><th>Total</th><th>OK</th><th>Failed</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for row in operation_breakdown %}
|
|
<tr>
|
|
<td>{{ row.operation|default:"(none)" }}</td>
|
|
<td>{{ row.total }}</td>
|
|
<td class="has-text-success">{{ row.ok }}</td>
|
|
<td class="has-text-danger">{{ row.failed }}</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr><td colspan="4">No runs yet.</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
<div class="column is-6">
|
|
<article class="card">
|
|
<header class="card-header">
|
|
<p class="card-header-title is-size-6">By Model</p>
|
|
</header>
|
|
<div class="card-content">
|
|
<div class="table-container">
|
|
<table class="table is-fullwidth is-size-7 is-striped is-hoverable">
|
|
<thead>
|
|
<tr><th>Model</th><th>Total</th><th>OK</th><th>Failed</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for row in model_breakdown %}
|
|
<tr>
|
|
<td>{{ row.model|default:"(none)" }}</td>
|
|
<td>{{ row.total }}</td>
|
|
<td class="has-text-success">{{ row.ok }}</td>
|
|
<td class="has-text-danger">{{ row.failed }}</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr><td colspan="4">No runs yet.</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
</div>
|
|
|
|
<article class="card">
|
|
<header class="card-header">
|
|
<p class="card-header-title is-size-6">Recent Runs</p>
|
|
</header>
|
|
<div class="card-content">
|
|
<div class="table-container">
|
|
<table class="table is-fullwidth is-size-7 is-striped is-hoverable">
|
|
<thead>
|
|
<tr>
|
|
<th></th>
|
|
<th>Started</th>
|
|
<th>Status</th>
|
|
<th>Operation</th>
|
|
<th>Model</th>
|
|
<th>Messages</th>
|
|
<th>Prompt</th>
|
|
<th>Response</th>
|
|
<th>Duration</th>
|
|
<th>Error</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for run in runs %}
|
|
<tr>
|
|
<td>
|
|
<button
|
|
class="button is-small is-light trace-run-expand"
|
|
type="button"
|
|
data-detail-row="trace-run-detail-{{ run.id }}"
|
|
data-detail-content="trace-run-detail-content-{{ run.id }}"
|
|
data-expanded-label="Hide"
|
|
data-collapsed-label="Show"
|
|
hx-get="{% url 'ai_execution_run_detail' run_id=run.id %}"
|
|
hx-target="#trace-run-detail-content-{{ run.id }}"
|
|
hx-swap="innerHTML"
|
|
hx-trigger="click once"
|
|
>
|
|
Show
|
|
</button>
|
|
</td>
|
|
<td>{{ run.started_at }}</td>
|
|
<td>
|
|
{% if run.status == "ok" %}
|
|
<span class="tag is-success is-light">ok</span>
|
|
{% elif run.status == "failed" %}
|
|
<span class="tag is-danger is-light">failed</span>
|
|
{% else %}
|
|
<span class="tag is-light">{{ run.status }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ run.operation|default:"-" }}</td>
|
|
<td>{{ run.model|default:"-" }}</td>
|
|
<td>{{ run.message_count }}</td>
|
|
<td>{{ run.prompt_chars }}</td>
|
|
<td>{{ run.response_chars }}</td>
|
|
<td>{% if run.duration_ms %}{{ run.duration_ms }}ms{% else %}-{% endif %}</td>
|
|
<td>
|
|
{% if run.error %}
|
|
<span title="{{ run.error }}">{{ run.error|truncatechars:120 }}</span>
|
|
{% else %}
|
|
-
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
<tr id="trace-run-detail-{{ run.id }}" class="is-hidden">
|
|
<td colspan="10">
|
|
<div id="trace-run-detail-content-{{ run.id }}" class="trace-run-detail-shell is-size-7 has-text-grey">
|
|
Click Show to load run details.
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr><td colspan="10">No runs yet.</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
<script>
|
|
(function () {
|
|
document.querySelectorAll(".trace-run-expand").forEach(function (button) {
|
|
button.addEventListener("click", function () {
|
|
const rowId = String(button.getAttribute("data-detail-row") || "");
|
|
const row = rowId ? document.getElementById(rowId) : null;
|
|
if (!row) {
|
|
return;
|
|
}
|
|
const isHidden = row.classList.contains("is-hidden");
|
|
row.classList.toggle("is-hidden", !isHidden);
|
|
button.textContent = isHidden
|
|
? String(button.getAttribute("data-expanded-label") || "Hide")
|
|
: String(button.getAttribute("data-collapsed-label") || "Show");
|
|
});
|
|
});
|
|
|
|
document.addEventListener("click", function (event) {
|
|
const trigger = event.target.closest(".trace-run-tab-trigger");
|
|
if (!trigger) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
const shell = trigger.closest(".trace-run-detail-tabs");
|
|
if (!shell) {
|
|
return;
|
|
}
|
|
const targetName = String(trigger.getAttribute("data-tab-target") || "");
|
|
if (!targetName) {
|
|
return;
|
|
}
|
|
shell.querySelectorAll(".trace-run-tab-trigger").forEach(function (item) {
|
|
item.parentElement.classList.toggle("is-active", item === trigger);
|
|
});
|
|
shell.querySelectorAll(".trace-run-tab-panel").forEach(function (panel) {
|
|
const isActive = panel.getAttribute("data-tab-panel") === targetName;
|
|
panel.classList.toggle("is-hidden", !isActive);
|
|
});
|
|
|
|
const lazyUrl = String(trigger.getAttribute("data-lazy-url") || "");
|
|
if (!lazyUrl) {
|
|
return;
|
|
}
|
|
const panel = shell.querySelector(
|
|
'.trace-run-tab-panel[data-tab-panel="' + targetName + '"]'
|
|
);
|
|
if (!panel || panel.getAttribute("data-loaded") === "1") {
|
|
return;
|
|
}
|
|
panel.setAttribute("data-loaded", "1");
|
|
panel.classList.add("is-loading");
|
|
fetch(lazyUrl, { credentials: "same-origin" })
|
|
.then(function (response) {
|
|
if (!response.ok) {
|
|
throw new Error("tab load failed");
|
|
}
|
|
return response.text();
|
|
})
|
|
.then(function (html) {
|
|
panel.innerHTML = html;
|
|
})
|
|
.catch(function () {
|
|
panel.innerHTML =
|
|
'<p class="has-text-danger">Unable to load this tab.</p>';
|
|
})
|
|
.finally(function () {
|
|
panel.classList.remove("is-loading");
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
{% endblock %}
|