Refactor to modular app factory architecture
- Replace 6847-line monolithic server.py with 26-line entry point - All routes now in app/routers/ using Jinja2 templates - Add plan_node.html template for step details - Add plan node route to runs router with cache_id lookup - Backup old server as server_legacy.py 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -309,6 +309,110 @@ async def run_artifacts(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{run_id}/plan/node/{cache_id}", response_class=HTMLResponse)
|
||||
async def plan_node_detail(
|
||||
run_id: str,
|
||||
cache_id: str,
|
||||
request: Request,
|
||||
run_service: RunService = Depends(get_run_service),
|
||||
):
|
||||
"""HTMX partial: Get plan node detail by cache_id."""
|
||||
from ..services.auth_service import AuthService
|
||||
from artdag_common import render_fragment
|
||||
|
||||
auth_service = AuthService(get_redis_client())
|
||||
ctx = auth_service.get_user_from_cookie(request)
|
||||
|
||||
if not ctx:
|
||||
return HTMLResponse('<p class="text-red-400">Login required</p>', status_code=401)
|
||||
|
||||
run = await run_service.get_run(run_id)
|
||||
if not run:
|
||||
return HTMLResponse('<p class="text-red-400">Run not found</p>', status_code=404)
|
||||
|
||||
plan = await run_service.get_run_plan(run_id)
|
||||
if not plan:
|
||||
return HTMLResponse('<p class="text-gray-400">Plan not found</p>')
|
||||
|
||||
# Build lookups
|
||||
steps_by_cache_id = {}
|
||||
steps_by_step_id = {}
|
||||
for s in plan.get("steps", []):
|
||||
if s.get("cache_id"):
|
||||
steps_by_cache_id[s["cache_id"]] = s
|
||||
if s.get("step_id"):
|
||||
steps_by_step_id[s["step_id"]] = s
|
||||
|
||||
step = steps_by_cache_id.get(cache_id)
|
||||
if not step:
|
||||
return HTMLResponse(f'<p class="text-gray-400">Step not found</p>')
|
||||
|
||||
cache_manager = get_cache_manager()
|
||||
|
||||
# Node colors
|
||||
node_colors = {
|
||||
"SOURCE": "#3b82f6", "EFFECT": "#22c55e", "OUTPUT": "#a855f7",
|
||||
"ANALYSIS": "#f59e0b", "_LIST": "#6366f1", "default": "#6b7280"
|
||||
}
|
||||
node_color = node_colors.get(step.get("node_type", "EFFECT"), node_colors["default"])
|
||||
|
||||
# Check cache status
|
||||
has_cached = cache_manager.has_content(cache_id) if cache_id else False
|
||||
|
||||
# Determine output media type
|
||||
output_media_type = None
|
||||
output_preview = False
|
||||
if has_cached:
|
||||
cache_path = cache_manager.get_content_path(cache_id)
|
||||
if cache_path:
|
||||
output_media_type = run_service.detect_media_type(cache_path)
|
||||
output_preview = output_media_type in ('video', 'image', 'audio')
|
||||
|
||||
# Check for IPFS CID
|
||||
ipfs_cid = None
|
||||
if run.step_results:
|
||||
res = run.step_results.get(step.get("step_id"))
|
||||
if isinstance(res, dict) and res.get("cid"):
|
||||
ipfs_cid = res["cid"]
|
||||
|
||||
# Build input previews
|
||||
inputs = []
|
||||
for inp_step_id in step.get("input_steps", []):
|
||||
inp_step = steps_by_step_id.get(inp_step_id)
|
||||
if inp_step:
|
||||
inp_cache_id = inp_step.get("cache_id", "")
|
||||
inp_has_cached = cache_manager.has_content(inp_cache_id) if inp_cache_id else False
|
||||
inp_media_type = None
|
||||
if inp_has_cached:
|
||||
inp_path = cache_manager.get_content_path(inp_cache_id)
|
||||
if inp_path:
|
||||
inp_media_type = run_service.detect_media_type(inp_path)
|
||||
|
||||
inputs.append({
|
||||
"name": inp_step.get("name", inp_step_id[:12]),
|
||||
"cache_id": inp_cache_id,
|
||||
"media_type": inp_media_type,
|
||||
"has_cached": inp_has_cached,
|
||||
})
|
||||
|
||||
status = "cached" if (has_cached or ipfs_cid) else ("completed" if run.status == "completed" else "pending")
|
||||
|
||||
templates = get_templates(request)
|
||||
return HTMLResponse(render_fragment(templates, "runs/plan_node.html",
|
||||
step=step,
|
||||
cache_id=cache_id,
|
||||
node_color=node_color,
|
||||
status=status,
|
||||
has_cached=has_cached,
|
||||
output_preview=output_preview,
|
||||
output_media_type=output_media_type,
|
||||
ipfs_cid=ipfs_cid,
|
||||
ipfs_gateway="https://ipfs.io/ipfs",
|
||||
inputs=inputs,
|
||||
config=step.get("config", {}),
|
||||
))
|
||||
|
||||
|
||||
@router.delete("/{run_id}/ui", response_class=HTMLResponse)
|
||||
async def ui_discard_run(
|
||||
run_id: str,
|
||||
|
||||
99
app/templates/runs/plan_node.html
Normal file
99
app/templates/runs/plan_node.html
Normal file
@@ -0,0 +1,99 @@
|
||||
{# Plan node detail panel - loaded via HTMX #}
|
||||
{% set status_color = 'green' if status in ('cached', 'completed') else 'yellow' %}
|
||||
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h4 class="text-lg font-semibold text-white">{{ step.name or step.step_id[:20] }}</h4>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<span class="px-2 py-0.5 rounded text-xs text-white" style="background-color: {{ node_color }}">
|
||||
{{ step.node_type or 'EFFECT' }}
|
||||
</span>
|
||||
<span class="text-{{ status_color }}-400 text-xs">{{ status }}</span>
|
||||
<span class="text-gray-500 text-xs">Level {{ step.level or 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="closeNodeDetail()" class="text-gray-400 hover:text-white p-1">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# Output preview #}
|
||||
{% if output_preview %}
|
||||
<div class="mb-4">
|
||||
<h5 class="text-sm font-medium text-gray-400 mb-2">Output</h5>
|
||||
{% if output_media_type == 'video' %}
|
||||
<video src="/cache/{{ cache_id }}/raw" controls muted class="w-full max-h-48 rounded-lg"></video>
|
||||
{% elif output_media_type == 'image' %}
|
||||
<img src="/cache/{{ cache_id }}/raw" class="w-full max-h-48 rounded-lg object-contain">
|
||||
{% elif output_media_type == 'audio' %}
|
||||
<audio src="/cache/{{ cache_id }}/raw" controls class="w-full"></audio>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif ipfs_cid %}
|
||||
<div class="mb-4">
|
||||
<h5 class="text-sm font-medium text-gray-400 mb-2">Output (IPFS)</h5>
|
||||
<video src="{{ ipfs_gateway }}/{{ ipfs_cid }}" controls muted class="w-full max-h-48 rounded-lg"></video>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Output link #}
|
||||
{% if ipfs_cid %}
|
||||
<a href="/ipfs/{{ ipfs_cid }}" class="flex items-center justify-between bg-gray-800 rounded p-2 hover:bg-gray-700 transition-colors text-xs mb-4">
|
||||
<span class="font-mono text-gray-300 truncate">{{ ipfs_cid[:24] }}...</span>
|
||||
<span class="px-2 py-1 bg-blue-600 text-white rounded ml-2">View</span>
|
||||
</a>
|
||||
{% elif has_cached and cache_id %}
|
||||
<a href="/cache/{{ cache_id }}" class="flex items-center justify-between bg-gray-800 rounded p-2 hover:bg-gray-700 transition-colors text-xs mb-4">
|
||||
<span class="font-mono text-gray-300 truncate">{{ cache_id[:24] }}...</span>
|
||||
<span class="px-2 py-1 bg-blue-600 text-white rounded ml-2">View</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{# Input media previews #}
|
||||
{% if inputs %}
|
||||
<div class="mt-4">
|
||||
<h5 class="text-sm font-medium text-gray-400 mb-2">Inputs ({{ inputs|length }})</h5>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
{% for inp in inputs %}
|
||||
<a href="/cache/{{ inp.cache_id }}" class="block bg-gray-800 rounded-lg overflow-hidden hover:bg-gray-700 transition-colors">
|
||||
{% if inp.media_type == 'video' %}
|
||||
<video src="/cache/{{ inp.cache_id }}/raw" class="w-full h-20 object-cover rounded-t" muted></video>
|
||||
{% elif inp.media_type == 'image' %}
|
||||
<img src="/cache/{{ inp.cache_id }}/raw" class="w-full h-20 object-cover rounded-t">
|
||||
{% else %}
|
||||
<div class="w-full h-20 bg-gray-700 rounded-t flex items-center justify-center text-xs text-gray-400">
|
||||
{{ inp.media_type or 'File' }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="p-2">
|
||||
<div class="text-xs text-white truncate">{{ inp.name }}</div>
|
||||
<div class="text-xs text-gray-500 font-mono truncate">{{ inp.cache_id[:12] }}...</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Parameters/Config #}
|
||||
{% if config %}
|
||||
<div class="mt-4">
|
||||
<h5 class="text-sm font-medium text-gray-400 mb-2">Parameters</h5>
|
||||
<div class="bg-gray-800 rounded p-3 text-xs space-y-1">
|
||||
{% for key, value in config.items() %}
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-400">{{ key }}:</span>
|
||||
<span class="text-white">{{ value if value is string else value|tojson }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Metadata #}
|
||||
<div class="mt-4 text-xs text-gray-500 space-y-1">
|
||||
<div><span class="text-gray-400">Step ID:</span> <span class="font-mono">{{ step.step_id[:32] }}...</span></div>
|
||||
<div><span class="text-gray-400">Cache ID:</span> <span class="font-mono">{{ cache_id[:32] }}...</span></div>
|
||||
</div>
|
||||
6847
server_legacy.py
Normal file
6847
server_legacy.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user