add heavy loggin

This commit is contained in:
2026-03-26 10:59:56 -03:00
parent a85722f96a
commit beb0416280
27 changed files with 502 additions and 64 deletions

View File

@@ -9,19 +9,27 @@ import FramePanel from './panels/FramePanel.vue'
import BrandTablePanel from './panels/BrandTablePanel.vue'
import TimelinePanel from './panels/TimelinePanel.vue'
import CostStatsPanel from './panels/CostStatsPanel.vue'
import SourceSelector from './panels/SourceSelector.vue'
import type { StatsUpdate, RunContext } from './types/sse-contract'
import { usePipelineStore } from './stores/pipeline'
const pipeline = usePipelineStore()
const jobId = ref(new URLSearchParams(window.location.search).get('job') || 'test-job')
const jobParam = new URLSearchParams(window.location.search).get('job')
const jobId = ref(jobParam || '')
const stats = ref<StatsUpdate | null>(null)
const runContext = ref<RunContext | null>(null)
const status = ref<'idle' | 'live' | 'processing' | 'error'>('idle')
const logPanel = ref<{ clear: () => void } | null>(null)
// No job selected → open source selector
if (!jobParam) {
pipeline.openSourceSelector()
}
const source = new SSEDataSource({
id: 'detect-stream',
url: `/api/detect/stream/${jobId.value}`,
url: jobId.value ? `/api/detect/stream/${jobId.value}` : '',
eventTypes: ['graph_update', 'stats_update', 'frame_update', 'detection', 'log', 'job_complete', 'waiting'],
})
@@ -37,6 +45,12 @@ source.on<StatsUpdate>('stats_update', (e) => {
}
})
source.on<{ report?: { status?: string, error?: string } }>('job_complete', (e) => {
if (e.report?.status === 'failed') {
status.value = 'error'
}
})
// Resizable splits
const pipelineWidth = ref(320)
const detectionsFlex = ref(3) // ratio for detections vs stats
@@ -71,7 +85,36 @@ const statusMap: Record<string, 'idle' | 'live' | 'processing' | 'error'> = {
const checkStatus = () => { status.value = statusMap[source.status.value] ?? 'idle' }
setInterval(checkStatus, 500)
source.connect()
if (jobId.value) {
source.connect()
}
async function stopPipeline() {
if (!jobId.value) return
try {
await fetch(`/api/detect/stop/${jobId.value}`, { method: 'POST' })
} catch { /* ignore — UI will see the cancel event via SSE */ }
}
function onJobStarted(newJobId: string) {
jobId.value = newJobId
// Reset UI state
stats.value = null
runContext.value = null
status.value = 'processing'
logPanel.value?.clear()
pipeline.reset()
pipeline.setStatus('running')
// Update URL without reload
const url = new URL(window.location.href)
url.searchParams.set('job', newJobId)
window.history.pushState({}, '', url.toString())
// Connect SSE to new job
source.disconnect()
source.setUrl(`/api/detect/stream/${newJobId}`)
source.connect()
// Switch to normal layout (reset sets it to normal already)
}
</script>
<template>
@@ -82,7 +125,18 @@ source.connect()
<span v-if="runContext" class="run-info">
{{ runContext.run_type }} · run: {{ runContext.run_id }}
</span>
<span class="job-id">job: {{ jobId }}</span>
<button class="header-btn" title="Select source" @click="pipeline.openSourceSelector()">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M2 4h4l2 2h6v8H2V4z"/><path d="M2 4V2h12v2"/>
</svg>
</button>
<button
v-if="jobId && (status === 'live' || status === 'processing')"
class="header-btn stop-btn"
title="Stop pipeline"
@click="stopPipeline"
></button>
<span class="job-id">job: {{ jobId || '—' }}</span>
</header>
<div class="main-layout">
@@ -168,6 +222,11 @@ source.connect()
</Panel>
</template>
<!-- === SOURCE SELECTOR MODE === -->
<template v-else-if="pipeline.layoutMode === 'source_selector'">
<SourceSelector @job-started="onJobStarted" />
</template>
</div>
</div>
@@ -183,7 +242,7 @@ source.connect()
</Panel>
</template>
<template v-else>
<LogPanel :source="source" :status="status" />
<LogPanel ref="logPanel" :source="source" :status="status" />
</template>
</div>
</div>
@@ -200,12 +259,11 @@ body {
}
.app {
height: 100vh;
min-height: 100vh;
display: grid;
grid-template-rows: auto 1fr auto;
padding: var(--space-4);
gap: var(--space-2);
overflow: hidden;
}
header {
@@ -235,6 +293,34 @@ header h1 { font-size: var(--font-size-lg); font-weight: 600; }
font-size: var(--font-size-sm);
}
.header-btn {
background: var(--surface-2);
border: 1px solid var(--surface-3);
border-radius: 4px;
color: var(--text-secondary);
width: 28px;
height: 28px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.header-btn:hover {
background: var(--surface-3);
color: var(--text-primary);
}
.stop-btn {
background: var(--status-error);
color: #000;
font-size: 12px;
font-weight: 700;
}
.stop-btn:hover {
opacity: 0.8;
}
.job-id { color: var(--text-dim); font-size: var(--font-size-sm); margin-left: auto; }
/* Main layout: pipeline left, content right — both same height */
@@ -328,11 +414,10 @@ header h1 { font-size: var(--font-size-lg); font-weight: 600; }
.stat .label { color: var(--text-dim); font-size: var(--font-size-sm); }
.stat .value { font-weight: 600; }
/* Log: full width bottom, fixed height */
/* Log: full width bottom */
.log-row {
flex-shrink: 0;
height: 160px;
overflow: hidden;
height: 200px;
}
.empty { color: var(--text-dim); padding: var(--space-6); text-align: center; }
@@ -399,4 +484,50 @@ header h1 { font-size: var(--font-size-lg); font-weight: 600; }
text-align: center;
font-size: var(--font-size-sm);
}
/* Source selector */
.source-selector {
display: flex;
flex-direction: column;
height: 100%;
gap: var(--space-3);
padding: var(--space-3);
}
.source-info {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.source-hint {
color: var(--text-dim);
margin-top: var(--space-1);
}
.source-hint code {
background: var(--surface-2);
padding: 1px 4px;
border-radius: 3px;
}
.source-list {
flex: 1;
overflow-y: auto;
background: var(--surface-2);
border-radius: var(--panel-radius);
padding: var(--space-2);
}
.source-loading {
color: var(--text-dim);
text-align: center;
padding: var(--space-4);
font-size: var(--font-size-sm);
}
.source-actions {
flex-shrink: 0;
display: flex;
justify-content: flex-end;
}
</style>