add heavy loggin
This commit is contained in:
@@ -170,19 +170,6 @@ export interface SourceBrandSighting {
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
export interface SourceJob {
|
||||
job_id: string;
|
||||
source_type: string;
|
||||
chunk_count: number;
|
||||
total_bytes: number;
|
||||
}
|
||||
|
||||
export interface ChunkInfo {
|
||||
filename: string;
|
||||
key: string;
|
||||
size_bytes: number;
|
||||
}
|
||||
|
||||
export interface CreateJobRequest {
|
||||
source_asset_id: string;
|
||||
preset_id: string | null;
|
||||
@@ -220,6 +207,19 @@ export interface WorkerStatus {
|
||||
gpu_available: boolean;
|
||||
}
|
||||
|
||||
export interface SourceJob {
|
||||
job_id: string;
|
||||
source_type: string;
|
||||
chunk_count: number;
|
||||
total_bytes: number;
|
||||
}
|
||||
|
||||
export interface ChunkInfo {
|
||||
filename: string;
|
||||
key: string;
|
||||
size_bytes: number;
|
||||
}
|
||||
|
||||
export interface ChunkEvent {
|
||||
sequence: number;
|
||||
status: string;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -21,6 +21,12 @@ props.source.on<LogEvent>('log', (e) => {
|
||||
ts: e.ts,
|
||||
})
|
||||
})
|
||||
|
||||
function clear() {
|
||||
entries.value = []
|
||||
}
|
||||
|
||||
defineExpose({ clear })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -26,6 +26,21 @@ props.source.on<{ nodes: GraphNode[] }>('graph_update', (e) => {
|
||||
nodes.value = e.nodes
|
||||
})
|
||||
|
||||
props.source.on<{ report?: { status?: string } }>('job_complete', (e) => {
|
||||
const status = e.report?.status
|
||||
if (status === 'failed' || status === 'cancelled') {
|
||||
nodes.value = nodes.value.map(n => ({
|
||||
...n,
|
||||
status: n.status === 'running' ? 'error' : n.status,
|
||||
}))
|
||||
} else {
|
||||
nodes.value = nodes.value.map(n => ({
|
||||
...n,
|
||||
status: n.status === 'running' ? 'done' : n.status,
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
function onOpenRegionEditor(stage: string) {
|
||||
pipeline.openBBoxEditor(stage)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export const usePipelineStore = defineStore('pipeline', () => {
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// Layout mode
|
||||
const layoutMode = ref<string>('normal') // normal | bbox_editor | stage_editor
|
||||
const layoutMode = ref<string>('normal') // normal | bbox_editor | stage_editor | source_selector
|
||||
const editorStage = ref<string | null>(null) // which stage's editor is open
|
||||
|
||||
const isRunning = computed(() => status.value === 'running')
|
||||
@@ -59,6 +59,11 @@ export const usePipelineStore = defineStore('pipeline', () => {
|
||||
if (msg) status.value = 'error'
|
||||
}
|
||||
|
||||
function openSourceSelector() {
|
||||
layoutMode.value = 'source_selector'
|
||||
editorStage.value = null
|
||||
}
|
||||
|
||||
function openBBoxEditor(stage: string) {
|
||||
layoutMode.value = 'bbox_editor'
|
||||
editorStage.value = stage
|
||||
@@ -91,6 +96,6 @@ export const usePipelineStore = defineStore('pipeline', () => {
|
||||
checkpoints, error, layoutMode, editorStage,
|
||||
isRunning, isPaused, canReplay, isEditing,
|
||||
setJob, setStatus, updateNodes, setRunContext, setCheckpoints, setError,
|
||||
openBBoxEditor, openStageEditor, closeEditor, reset,
|
||||
openSourceSelector, openBBoxEditor, openStageEditor, closeEditor, reset,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -9,6 +9,7 @@ defineProps<{
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">{{ title }}</span>
|
||||
<span class="panel-actions"><slot name="actions" /></span>
|
||||
<span class="panel-status" :class="status ?? 'idle'" />
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
@@ -51,11 +52,17 @@ defineProps<{
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.panel-actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.panel-status {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-left: auto;
|
||||
}
|
||||
.panel-status.idle { background: var(--status-idle); }
|
||||
.panel-status.live { background: var(--status-live); }
|
||||
|
||||
@@ -69,10 +69,9 @@ export class SSEDataSource extends DataSource {
|
||||
})
|
||||
}
|
||||
|
||||
// Also listen to the generic 'done' terminal event
|
||||
// Terminal event — pipeline finished (success, failure, or cancel)
|
||||
this.es.addEventListener('done', () => {
|
||||
this.status.value = 'idle'
|
||||
this.disconnect()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import '@vue-flow/core/dist/theme-default.css'
|
||||
|
||||
export interface GraphNode {
|
||||
id: string
|
||||
status: 'pending' | 'running' | 'done' | 'error'
|
||||
status: 'pending' | 'running' | 'done' | 'error' | 'skipped'
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -29,6 +29,7 @@ const statusColors: Record<string, string> = {
|
||||
running: 'var(--status-processing)',
|
||||
done: 'var(--status-live)',
|
||||
error: 'var(--status-error)',
|
||||
skipped: '#4a6fa5',
|
||||
}
|
||||
|
||||
const flowNodes = computed(() =>
|
||||
|
||||
Reference in New Issue
Block a user