Files
celery/app/templates/runs/detail.html
giles 945fb3b413 Fix analysis display for recipe-based runs
Add get_run_analysis() to RunService to load per-input analysis from
CACHE_DIR/analysis/{hash}.json files. Update runs router and template
to display tempo, beats, energy, and beat timeline visualization.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 08:05:40 +00:00

325 lines
14 KiB
HTML

{% extends "base.html" %}
{% block title %}Run {{ run.run_id[:12] }} - Art-DAG L1{% endblock %}
{% block head %}
{{ super() }}
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.23.0/cytoscape.min.js"></script>
{% endblock %}
{% block content %}
{% set status_colors = {'completed': 'green', 'running': 'blue', 'pending': 'yellow', 'failed': 'red'} %}
{% set color = status_colors.get(run.status, 'gray') %}
<div class="max-w-6xl mx-auto">
<!-- Header -->
<div class="flex items-center space-x-4 mb-6">
<a href="/runs" class="text-gray-400 hover:text-white">&larr; Runs</a>
<h1 class="text-2xl font-bold font-mono">{{ run.run_id[:16] }}...</h1>
<span class="bg-{{ color }}-900 text-{{ color }}-300 px-3 py-1 rounded text-sm uppercase">
{{ run.status }}
</span>
{% if run.cached %}
<span class="bg-purple-900 text-purple-300 px-3 py-1 rounded text-sm">Cached</span>
{% endif %}
</div>
<!-- Info Grid -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div class="bg-gray-800 rounded-lg p-4">
<div class="text-gray-500 text-sm">Recipe</div>
<div class="text-white font-medium">{{ run.recipe or 'Unknown' }}</div>
</div>
<div class="bg-gray-800 rounded-lg p-4">
<div class="text-gray-500 text-sm">Steps</div>
<div class="text-white font-medium">
{{ run.executed or 0 }} / {{ run.total_steps or '?' }}
{% if run.cached_steps %}
<span class="text-purple-400 text-sm">({{ run.cached_steps }} cached)</span>
{% endif %}
</div>
</div>
<div class="bg-gray-800 rounded-lg p-4">
<div class="text-gray-500 text-sm">Created</div>
<div class="text-white font-medium">{{ run.created_at }}</div>
</div>
<div class="bg-gray-800 rounded-lg p-4">
<div class="text-gray-500 text-sm">User</div>
<div class="text-white font-medium">{{ run.username or 'Unknown' }}</div>
</div>
</div>
<!-- Tabs -->
<div class="border-b border-gray-700 mb-6">
<nav class="flex space-x-8">
<a href="#plan" class="tab-link border-b-2 border-blue-500 text-white pb-3 px-1"
onclick="showTab('plan')">Plan</a>
<a href="#artifacts" class="tab-link border-b-2 border-transparent text-gray-400 hover:text-white pb-3 px-1"
onclick="showTab('artifacts')">Artifacts</a>
{% if analysis %}
<a href="#analysis" class="tab-link border-b-2 border-transparent text-gray-400 hover:text-white pb-3 px-1"
onclick="showTab('analysis')">Analysis</a>
{% endif %}
<a href="#inputs" class="tab-link border-b-2 border-transparent text-gray-400 hover:text-white pb-3 px-1"
onclick="showTab('inputs')">Inputs</a>
</nav>
</div>
<!-- Plan Tab -->
<div id="tab-plan" class="tab-content">
{% if plan %}
<div id="dag-container" class="bg-gray-900 rounded-lg border border-gray-700 h-96 mb-4"></div>
<div class="space-y-2">
{% for step in plan.steps %}
{% set step_color = 'green' if step.status == 'completed' or step.cache_id else ('purple' if step.cached else ('blue' if step.status == 'running' else 'gray')) %}
<div class="bg-gray-800 rounded p-3">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<span class="w-6 h-6 rounded-full bg-{{ step_color }}-600 flex items-center justify-center text-xs">
{{ loop.index }}
</span>
<span class="font-medium">{{ step.name }}</span>
<span class="text-gray-500 text-sm">{{ step.type }}</span>
</div>
<div class="flex items-center space-x-3">
{% if step.cached %}
<span class="text-purple-400 text-sm">cached</span>
{% elif step.status == 'completed' %}
<span class="text-green-400 text-sm">completed</span>
{% endif %}
</div>
</div>
{% if step.cache_id %}
<div class="mt-2 ml-9 flex items-center space-x-2">
<span class="text-gray-500 text-xs">Output:</span>
<a href="/cache/{{ step.cache_id }}" class="font-mono text-xs text-blue-400 hover:text-blue-300">
{{ step.cache_id }}
</a>
</div>
{% endif %}
{% if step.outputs and step.outputs | length > 1 %}
<div class="mt-1 ml-9">
<span class="text-gray-500 text-xs">Additional outputs:</span>
{% for output in step.outputs %}
{% if output != step.cache_id %}
<a href="/cache/{{ output }}" class="block font-mono text-xs text-gray-400 hover:text-white ml-2">
{{ output[:32] }}...
</a>
{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
<!-- Plan JSON -->
<details class="mt-6">
<summary class="cursor-pointer text-gray-400 hover:text-white text-sm mb-2">
Show Plan JSON
</summary>
<div class="bg-gray-900 rounded-lg border border-gray-700 p-4 overflow-x-auto">
<pre class="text-sm text-gray-300 whitespace-pre-wrap">{{ plan | tojson(indent=2) }}</pre>
</div>
</details>
{% else %}
<p class="text-gray-500">No plan available for this run.</p>
{% endif %}
</div>
<!-- Artifacts Tab -->
<div id="tab-artifacts" class="tab-content hidden">
{% if artifacts %}
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{% for artifact in artifacts %}
<a href="/cache/{{ artifact.hash }}"
class="bg-gray-800 rounded-lg p-4 hover:bg-gray-750 transition-colors">
{% if artifact.media_type and artifact.media_type.startswith('image/') %}
<img src="/cache/{{ artifact.hash }}/raw" alt=""
class="w-full h-32 object-cover rounded mb-2">
{% elif artifact.media_type and artifact.media_type.startswith('video/') %}
<video src="/cache/{{ artifact.hash }}/raw"
class="w-full h-32 object-cover rounded mb-2" muted></video>
{% else %}
<div class="w-full h-32 bg-gray-900 rounded mb-2 flex items-center justify-center text-gray-600">
{{ artifact.media_type or 'Unknown' }}
</div>
{% endif %}
<div class="font-mono text-xs text-gray-500 truncate">{{ artifact.hash[:16] }}...</div>
<div class="text-sm text-gray-400">{{ artifact.step_name }}</div>
</a>
{% endfor %}
</div>
{% else %}
<p class="text-gray-500">No artifacts generated yet.</p>
{% endif %}
</div>
<!-- Analysis Tab -->
<div id="tab-analysis" class="tab-content hidden">
{% if analysis %}
<div class="space-y-6">
{% for item in analysis %}
<div class="bg-gray-800 rounded-lg p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold">{{ item.input_name }}</h3>
<a href="/cache/{{ item.input_hash }}" class="font-mono text-xs text-blue-400 hover:text-blue-300">
{{ item.input_hash[:16] }}...
</a>
</div>
{% if item.has_analysis %}
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
{% if item.tempo %}
<div class="bg-gray-900 rounded p-3">
<div class="text-gray-500 text-sm">Tempo</div>
<div class="text-2xl font-bold text-pink-400">{{ item.tempo | round(1) }}</div>
<div class="text-gray-500 text-xs">BPM</div>
</div>
{% endif %}
{% if item.beat_times %}
<div class="bg-gray-900 rounded p-3">
<div class="text-gray-500 text-sm">Beats</div>
<div class="text-2xl font-bold text-blue-400">{{ item.beat_times | length }}</div>
<div class="text-gray-500 text-xs">detected</div>
</div>
{% endif %}
{% if item.energy is not none %}
<div class="bg-gray-900 rounded p-3">
<div class="text-gray-500 text-sm">Energy</div>
<div class="text-2xl font-bold text-green-400">{{ (item.energy * 100) | round(1) }}%</div>
<div class="text-gray-500 text-xs">average</div>
</div>
{% endif %}
{% if item.beat_times %}
<div class="bg-gray-900 rounded p-3">
<div class="text-gray-500 text-sm">Duration</div>
<div class="text-2xl font-bold text-purple-400">
{{ (item.beat_times[-1] | default(0)) | round(1) }}s
</div>
<div class="text-gray-500 text-xs">analyzed</div>
</div>
{% endif %}
</div>
<!-- Beat visualization -->
{% if item.beat_times and item.beat_times | length > 0 %}
<div class="mt-4">
<div class="text-gray-500 text-sm mb-2">Beat Timeline</div>
<div class="bg-gray-900 rounded h-8 relative overflow-hidden">
{% set max_time = item.beat_times[-1] if item.beat_times else 1 %}
{% for beat_time in item.beat_times[:100] %}
<div class="absolute top-0 bottom-0 w-px bg-pink-500 opacity-60"
style="left: {{ (beat_time / max_time * 100) | round(2) }}%"></div>
{% endfor %}
</div>
<div class="flex justify-between text-xs text-gray-600 mt-1">
<span>0s</span>
<span>{{ max_time | round(1) }}s</span>
</div>
</div>
{% endif %}
<!-- Raw data toggle -->
<details class="mt-4">
<summary class="cursor-pointer text-gray-400 hover:text-white text-sm">
Show Raw Analysis Data
</summary>
<div class="bg-gray-900 rounded-lg border border-gray-700 p-4 mt-2 overflow-x-auto">
<pre class="text-sm text-gray-300 whitespace-pre-wrap">{{ item.raw | tojson(indent=2) }}</pre>
</div>
</details>
{% else %}
<p class="text-gray-500">No analysis data available for this input.</p>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<p class="text-gray-500">No inputs with analysis data.</p>
{% endif %}
</div>
<!-- Inputs Tab -->
<div id="tab-inputs" class="tab-content hidden">
{% if run.inputs %}
<div class="space-y-2">
{% for input_hash in run.inputs %}
<a href="/cache/{{ input_hash }}"
class="block bg-gray-800 rounded p-3 font-mono text-sm hover:bg-gray-750">
{{ input_hash }}
</a>
{% endfor %}
</div>
{% else %}
<p class="text-gray-500">No inputs recorded.</p>
{% endif %}
</div>
<!-- Output -->
{% if run.output_hash %}
<div class="mt-8 bg-gray-800 rounded-lg p-6">
<h3 class="text-lg font-semibold mb-4">Output</h3>
<div class="flex items-center justify-between">
<a href="/cache/{{ run.output_hash }}" class="font-mono text-blue-400 hover:text-blue-300">
{{ run.output_hash }}
</a>
{% if run.output_ipfs_cid %}
<a href="https://ipfs.io/ipfs/{{ run.output_ipfs_cid }}"
target="_blank"
class="text-gray-400 hover:text-white text-sm">
IPFS: {{ run.output_ipfs_cid[:16] }}...
</a>
{% endif %}
</div>
</div>
{% endif %}
</div>
<script>
function showTab(name) {
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
document.querySelectorAll('.tab-link').forEach(el => {
el.classList.remove('border-blue-500', 'text-white');
el.classList.add('border-transparent', 'text-gray-400');
});
document.getElementById('tab-' + name).classList.remove('hidden');
event.target.classList.add('border-blue-500', 'text-white');
event.target.classList.remove('border-transparent', 'text-gray-400');
}
{% if plan %}
// Initialize DAG
document.addEventListener('DOMContentLoaded', function() {
const cy = cytoscape({
container: document.getElementById('dag-container'),
style: [
{ selector: 'node', style: {
'label': 'data(label)',
'background-color': 'data(color)',
'color': '#fff',
'text-valign': 'center',
'font-size': '10px',
'width': 40,
'height': 40
}},
{ selector: 'edge', style: {
'width': 2,
'line-color': '#4b5563',
'target-arrow-color': '#4b5563',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier'
}}
],
elements: {{ dag_elements | tojson }},
layout: { name: 'dagre', rankDir: 'LR', padding: 30 }
});
});
{% endif %}
</script>
{% endblock %}