fix: load cluster names before saving to cache

- Changed order: loadClusterNames now runs BEFORE setClusterResults
- Removed condition that skipped loading names for 'Person_X' clusters
- Now always loads custom names from trace profiles (DB is source of truth)
- Fixes rename persistence - custom names are now correctly saved to cache
This commit is contained in:
2026-10-30 23:24:48 +08:00
parent 98aacc3d9c
commit 48bcd84528
2 changed files with 8 additions and 20 deletions

View File

@@ -335,20 +335,7 @@ export async function pollClusterResults(fileUuid: string, onDone?: (results: an
export async function loadClusterNames(fileUuid: string, clusters: any[]) { export async function loadClusterNames(fileUuid: string, clusters: any[]) {
console.log('[loadClusterNames] loading names for', clusters.length, 'clusters') console.log('[loadClusterNames] loading names for', clusters.length, 'clusters')
const promises = clusters.map(async (c) => { const promises = clusters.map(async (c) => {
// Only load name from DB if cluster has no meaningful name // Always load name from trace profile (DB is source of truth for custom names)
// "Person_X" names from clustering are considered meaningful
// Only replace "Unassigned", empty, or auto-generated trace names
const currentName = c.name || ''
const isAutoGenerated = currentName === 'Unassigned' ||
currentName === '' ||
currentName.startsWith('Face Trace ') ||
currentName.startsWith('Trace ')
if (!isAutoGenerated) {
// Cluster already has a meaningful name (e.g., Person_X from clustering or custom name), don't overwrite
return
}
const repTid = c.representative_trace !== undefined && c.representative_trace !== null ? c.representative_trace : c.trace_ids?.[0] const repTid = c.representative_trace !== undefined && c.representative_trace !== null ? c.representative_trace : c.trace_ids?.[0]
if (repTid != null) { if (repTid != null) {
try { try {

View File

@@ -296,10 +296,10 @@
<!-- Assign modal --> <!-- Assign modal -->
<!-- Face detail modal --> <!-- Face detail modal -->
<!-- Side navigation arrows - outside overlay for proper z-index stacking --> <!-- Side navigation arrows - outside overlay for proper z-index stacking -->
<button v-if="faceDetailModal.show && canFaceDetailPrev" class="ms-fs-nav-arrow ms-fs-nav-prev" @click="goFaceDetailPrev" title="Previous FS (←)" :style="{ zIndex: faceDetailZIndex + 100 }"> <button v-if="faceDetailModal.show && canFaceDetailPrev" class="ms-fs-nav-arrow ms-fs-nav-prev" @click="() => { navClickCount++; console.log('[nav] PREV CLICKED'); goFaceDetailPrev(); }" title="Previous FS (←)" :style="{ zIndex: faceDetailZIndex + 100, pointerEvents: 'auto' }">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><polyline points="15 18 9 12 15 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="24" height="24" viewBox="0 0 24 24" fill="none"><polyline points="15 18 9 12 15 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button> </button>
<button v-if="faceDetailModal.show && canFaceDetailNext" class="ms-fs-nav-arrow ms-fs-nav-next" @click="goFaceDetailNext" title="Next FS (→)" :style="{ zIndex: faceDetailZIndex + 100 }"> <button v-if="faceDetailModal.show && canFaceDetailNext" class="ms-fs-nav-arrow ms-fs-nav-next" @click="() => { navClickCount++; console.log('[nav] NEXT CLICKED'); goFaceDetailNext(); }" title="Next FS (→)" :style="{ zIndex: faceDetailZIndex + 100, pointerEvents: 'auto' }">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><polyline points="9 18 15 12 9 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="24" height="24" viewBox="0 0 24 24" fill="none"><polyline points="9 18 15 12 9 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button> </button>
<div v-if="faceDetailModal.show" class="ms-modal-overlay show" :style="{ zIndex: faceDetailZIndex }" @click.self="faceDetailModal.show = false"> <div v-if="faceDetailModal.show" class="ms-modal-overlay show" :style="{ zIndex: faceDetailZIndex }" @click.self="faceDetailModal.show = false">
@@ -326,7 +326,7 @@
<div class="ms-face-detail-info"> <div class="ms-face-detail-info">
<div class="ms-face-detail-title"> <div class="ms-face-detail-title">
<span class="ms-face-trace-id">{{ faceDetailModal.candidate?.file_uuid }}:FS#{{ faceDetailModal.candidate?.trace_id }}</span> <span class="ms-face-trace-id">{{ faceDetailModal.candidate?.file_uuid }}:FS#{{ faceDetailModal.candidate?.trace_id }}</span>
<span class="ms-nav-debug" style="font-size:10px;color:#888;margin-left:8px;">nav:{{ faceDetailNavSource || 'null' }} idx:{{ faceDetailNavIndex }} list:{{ faceDetailNavList.length }}</span> <span class="ms-nav-debug" style="font-size:10px;color:#888;margin-left:8px;">nav:{{ faceDetailNavSource || 'null' }} idx:{{ faceDetailNavIndex }} list:{{ faceDetailNavList.length }} clicks:{{ navClickCount }}</span>
</div> </div>
<div class="ms-face-detail-row"> <div class="ms-face-detail-row">
<span class="ms-face-detail-value">F{{ faceDetailModal.candidate?.start_frame }}–[F{{ faceDetailModal.candidate?.frame_number }}]–F{{ faceDetailModal.candidate?.end_frame }} {{ faceDetailModal.candidate?.frame_count }} Frames</span> <span class="ms-face-detail-value">F{{ faceDetailModal.candidate?.start_frame }}–[F{{ faceDetailModal.candidate?.frame_number }}]–F{{ faceDetailModal.candidate?.end_frame }} {{ faceDetailModal.candidate?.frame_count }} Frames</span>
@@ -1003,6 +1003,7 @@ const traceProfileJsonTitle = ref('')
const faceDetailModal = ref({ show: false, candidate: null as any, newIdentityName: '' }) const faceDetailModal = ref({ show: false, candidate: null as any, newIdentityName: '' })
const faceDetailNavSource = ref<'unassigned' | 'cluster' | null>(null) const faceDetailNavSource = ref<'unassigned' | 'cluster' | null>(null)
const faceDetailNavIndex = ref(-1) const faceDetailNavIndex = ref(-1)
const navClickCount = ref(0)
const regeneratingThumbnail = ref(false) const regeneratingThumbnail = ref(false)
const clusterDetailModal = ref({ show: false, cluster: null as any, selectedTrace: null as any }) const clusterDetailModal = ref({ show: false, cluster: null as any, selectedTrace: null as any })
const clusterVideoTraces = ref<any[]>([]) const clusterVideoTraces = ref<any[]>([])
@@ -2127,14 +2128,14 @@ async function loadClusterResults() {
} }
} }
if (groups.length) { if (groups.length) {
// Load trace profiles for names FIRST (DB is source of truth for custom names)
await loadClusterNames(fileUuid, groups)
console.log('[loadClusterResults] Calling setClusterResults with', groups.length, 'groups') console.log('[loadClusterResults] Calling setClusterResults with', groups.length, 'groups')
console.log('[loadClusterResults] First group trace_ids:', groups[0]?.trace_ids) console.log('[loadClusterResults] First group trace_ids:', groups[0]?.trace_ids)
setClusterResults(fileUuid, groups) setClusterResults(fileUuid, groups)
console.log('[loadClusterResults] After setClusterResults, cache:', clusterResultsCache.value[fileUuid]?.length) console.log('[loadClusterResults] After setClusterResults, cache:', clusterResultsCache.value[fileUuid]?.length)
// Load trace profiles for names in background (non-blocking)
loadClusterNames(fileUuid, groups).catch(() => {})
// Load thumbnails for cluster representatives immediately. // Load thumbnails for cluster representatives immediately.
// rep 資料只需 file_uuid + trace_id,皆已存在 cluster-results payload 中, // rep 資料只需 file_uuid + trace_id,皆已存在 cluster-results payload 中,
// 不需等待慢速的 get_unassigned_traces(~5s/page)。 // 不需等待慢速的 get_unassigned_traces(~5s/page)。