feat(trip-cluster): master/detail review routes + templates + clusterReview JS

This commit is contained in:
2026-06-27 17:35:58 +02:00
parent 44c05444d3
commit edf0735098
6 changed files with 508 additions and 3 deletions
@@ -0,0 +1,71 @@
{% extends "base.html" %}
{% from "macros.html" import confidence_badge, status_badge %}
{% block title %}trip-cluster{% endblock %}
{% block navbar_title %}trip-cluster{% endblock %}
{% block content %}
{% if stats.clusters == 0 %}
<div class="hero py-20"><div class="hero-content text-center"><div>
<h1 class="text-2xl font-bold">No clusters yet</h1>
{% if stats.assets == 0 %}
<p class="opacity-70 mt-2">Nothing ingested. Run
<code>categorize ingest</code> then <code>categorize cluster</code>.</p>
{% else %}
<p class="opacity-70 mt-2">{{ stats.assets }} assets ingested, but this scope
produced no clusters. Try a wider <code>categorize ingest</code> scope.</p>
{% endif %}
</div></div></div>
{% else %}
<div x-data="clusterReview()" @keydown.window="onKey($event)" class="flex gap-4">
<div class="w-72 shrink-0 max-h-[85vh] overflow-y-auto">
<div class="flex items-center justify-between mb-1">
<span class="font-bold">Clusters</span>
<button class="btn btn-xs btn-primary" @click="approveHighConfidence()">Approve high-conf</button>
</div>
<!-- F8: explain the attention sort -->
<p class="text-xs opacity-60 mb-2">Needs attention first — lowest-confidence pending.</p>
{% if stats.pending == 0 %}
<div class="alert alert-success text-xs mb-2">All reviewed — “Apply all” or <code>categorize apply</code>.</div>
{% endif %}
<!-- F4: write-failed clusters surfaced here after Apply all -->
<div x-show="failures.length" class="alert alert-error text-xs mb-2 flex-col items-start">
<span class="font-semibold">Write failures:</span>
<ul class="list-disc ml-4">
<template x-for="f in failures" :key="f.id">
<li><span x-text="f.name"></span><span x-text="f.failed"></span> failed</li>
</template>
</ul>
</div>
{% for c in clusters %}
<div class="cluster-row block p-2 rounded cursor-pointer hover:bg-base-300"
:class="selected == {{ c.id }} && 'bg-base-300'"
data-cluster-id="{{ c.id }}" data-cluster-name="{{ c.decided_name or c.suggested_name }}"
@click="selectCluster({{ c.id }})">
<div class="flex items-center justify-between gap-1">
<span class="truncate text-sm font-medium">{{ c.decided_name or c.suggested_name }}</span>
{{ confidence_badge(c.confidence) }}
</div>
<div class="flex items-center gap-1 mt-1">
<span class="cluster-status" data-cluster-id="{{ c.id }}">{{ status_badge(c.status) }}</span>
<span class="badge badge-xs">{{ c.count }}</span>
<span class="text-xs opacity-50">{{ c.start_at[:10] }}</span>
</div>
</div>
{% endfor %}
<button class="btn btn-xs btn-block mt-3" @click="applyAll()">Apply all approved</button>
</div>
<div class="flex-1 relative">
<!-- F16: loading indicator for declarative + programmatic swaps -->
<div id="detail-indicator" class="htmx-indicator absolute top-2 right-2 z-10">
<span class="loading loading-spinner loading-md"></span>
</div>
<div id="detail" class="flex-1"
hx-get="/cluster/{{ clusters[0].id }}" hx-trigger="load" hx-target="#detail"
hx-indicator="#detail-indicator"></div>
</div>
</div>
<div class="fixed bottom-2 right-2 text-xs opacity-50">
[ ] cluster · ← → grid · Enter open · A approve · N non-trip · S split (focus a photo) · X skip · Esc close
</div>
{% endif %}
{% endblock %}
{% block extra_scripts %}<script src="/static/app.js"></script>{% endblock %}