refactor storage minio for k8s

This commit is contained in:
2026-03-26 09:20:23 -03:00
parent e27cb5bcc3
commit c9ba9e4f5f
22 changed files with 961 additions and 18 deletions

View File

@@ -0,0 +1,386 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Panel } from 'mpr-ui-framework'
import { usePipelineStore } from '../stores/pipeline'
const pipeline = usePipelineStore()
interface ChunkInfo {
filename: string
key: string
size_bytes: number
}
interface SourceInfo {
job_id: string
source_type: string
chunk_count: number
total_bytes: number
}
const SOURCE_TYPE_LABELS: Record<string, string> = {
chunk_job: 'CHUNKS',
upload: 'UPLOAD',
device: 'DEVICE',
stream: 'STREAM',
}
const sources = ref<SourceInfo[]>([])
const chunks = ref<ChunkInfo[]>([])
const selectedSource = ref<string | null>(null)
const selectedChunk = ref<string | null>(null)
const loading = ref(false)
const running = ref(false)
const skipVlm = ref(false)
const skipCloud = ref(true)
const checkpoint = ref(true)
const logLevel = ref('INFO')
const error = ref<string | null>(null)
async function loadSources() {
loading.value = true
error.value = null
try {
const resp = await fetch('/api/detect/sources')
if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`)
sources.value = await resp.json()
} catch (e: any) {
error.value = `Failed to load sources: ${e.message}`
} finally {
loading.value = false
}
}
async function loadChunks(jobId: string) {
selectedSource.value = jobId
selectedChunk.value = null
chunks.value = []
try {
const resp = await fetch(`/api/detect/sources/${jobId}/chunks`)
if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`)
chunks.value = await resp.json()
} catch (e: any) {
error.value = `Failed to load chunks: ${e.message}`
}
}
function selectChunk(chunk: ChunkInfo) {
selectedChunk.value = chunk.key
}
async function openPreview(chunk: ChunkInfo) {
if (!selectedSource.value) return
try {
const resp = await fetch(
`/api/detect/sources/${selectedSource.value}/chunks/${encodeURIComponent(chunk.filename)}/url`
)
if (!resp.ok) throw new Error(`${resp.status}`)
const data = await resp.json()
window.open(data.url, '_blank')
} catch (e: any) {
error.value = `Could not get preview URL: ${e.message}`
}
}
const emit = defineEmits<{
(e: 'job-started', jobId: string): void
}>()
async function runPipeline() {
if (!selectedChunk.value) return
running.value = true
error.value = null
try {
const resp = await fetch('/api/detect/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
video_path: selectedChunk.value,
checkpoint: checkpoint.value,
skip_vlm: skipVlm.value,
skip_cloud: skipCloud.value,
log_level: logLevel.value,
}),
})
if (!resp.ok) {
const detail = await resp.text()
throw new Error(`${resp.status}: ${detail}`)
}
const data = await resp.json()
emit('job-started', data.job_id)
} catch (e: any) {
error.value = `Failed to start pipeline: ${e.message}`
running.value = false
}
}
function formatSize(bytes: number): string {
if (bytes < 1024) return `${bytes}B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}KB`
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`
}
function sourceTypeLabel(sourceType: string): string {
return SOURCE_TYPE_LABELS[sourceType] ?? sourceType.toUpperCase()
}
onMounted(loadSources)
</script>
<template>
<Panel title="Select Source">
<div class="source-selector">
<div v-if="error" class="source-error">{{ error }}</div>
<!-- Source list -->
<div class="source-section">
<h3>Chunk Jobs</h3>
<div class="source-list" v-if="!loading">
<div
v-for="src in sources"
:key="src.job_id"
:class="['source-item', { selected: selectedSource === src.job_id }]"
@click="loadChunks(src.job_id)"
>
<span class="source-id">{{ src.job_id.slice(0, 12) }}</span>
<span class="source-meta">
<span class="source-type-badge">{{ sourceTypeLabel(src.source_type) }}</span>
<span class="source-count">{{ src.chunk_count }} chunks</span>
<span class="source-size">{{ formatSize(src.total_bytes) }}</span>
</span>
</div>
<div v-if="sources.length === 0" class="source-empty">No sources found</div>
</div>
<div v-else class="source-empty">Loading...</div>
</div>
<!-- Chunk list -->
<div class="source-section" v-if="chunks.length > 0">
<h3>Chunks</h3>
<div class="chunk-list">
<div
v-for="chunk in chunks"
:key="chunk.key"
:class="['chunk-item', { selected: selectedChunk === chunk.key }]"
@click="selectChunk(chunk)"
>
<span class="chunk-name">{{ chunk.filename }}</span>
<span class="chunk-meta">
<span class="chunk-size">{{ formatSize(chunk.size_bytes) }}</span>
<button
class="preview-btn"
@click.stop="openPreview(chunk)"
title="Open preview"
></button>
</span>
</div>
</div>
</div>
<!-- Run options -->
<div class="run-options" v-if="selectedChunk">
<h3>Run Options</h3>
<label><input type="checkbox" v-model="checkpoint"> Checkpointing</label>
<label><input type="checkbox" v-model="skipVlm"> Skip VLM</label>
<label><input type="checkbox" v-model="skipCloud"> Skip Cloud</label>
<label>
Log level
<select v-model="logLevel" class="log-level-select">
<option value="INFO">INFO</option>
<option value="DEBUG">DEBUG</option>
</select>
</label>
<div class="selected-path">{{ selectedChunk }}</div>
<button class="run-btn" @click="runPipeline" :disabled="running">
{{ running ? 'Starting...' : 'Run Pipeline' }}
</button>
</div>
<div class="source-actions">
<button class="editor-close" @click="pipeline.closeEditor()"> Close</button>
</div>
</div>
</Panel>
</template>
<style scoped>
.source-selector {
display: flex;
flex-direction: column;
height: 100%;
gap: var(--space-3);
padding: var(--space-2);
}
.source-error {
color: var(--status-error);
font-size: var(--font-size-sm);
padding: var(--space-2);
background: rgba(224, 82, 82, 0.1);
border-radius: 4px;
}
.source-section {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.source-section h3 {
font-size: var(--font-size-sm);
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.source-list, .chunk-list {
max-height: 200px;
overflow-y: auto;
background: var(--surface-2);
border-radius: var(--panel-radius);
padding: var(--space-1);
}
.source-item, .chunk-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-1) var(--space-2);
border-radius: 3px;
cursor: pointer;
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.source-item:hover, .chunk-item:hover {
background: var(--surface-3);
}
.source-item.selected, .chunk-item.selected {
background: var(--surface-3);
color: var(--text-primary);
font-weight: 600;
}
.source-id { font-family: var(--font-mono); }
.source-meta {
display: flex;
align-items: center;
gap: var(--space-2);
}
.source-type-badge {
font-family: var(--font-mono);
font-size: 10px;
font-weight: 700;
color: var(--status-live);
background: rgba(0, 255, 128, 0.1);
border-radius: 2px;
padding: 1px 4px;
}
.source-count, .chunk-size, .source-size { color: var(--text-dim); font-size: 11px; }
.log-level-select {
background: var(--surface-2);
border: 1px solid var(--surface-3);
border-radius: 3px;
color: var(--text-secondary);
font-family: var(--font-mono);
font-size: var(--font-size-sm);
padding: 2px 4px;
margin-left: var(--space-2);
}
.source-empty { color: var(--text-dim); text-align: center; padding: var(--space-3); font-size: var(--font-size-sm); }
.chunk-meta {
display: flex;
align-items: center;
gap: var(--space-2);
}
.preview-btn {
background: none;
border: 1px solid var(--surface-3);
border-radius: 3px;
color: var(--text-dim);
font-size: 10px;
padding: 1px 5px;
cursor: pointer;
line-height: 1;
}
.preview-btn:hover {
background: var(--surface-3);
color: var(--text-primary);
}
.run-options {
display: flex;
flex-direction: column;
gap: var(--space-2);
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.run-options label {
display: flex;
align-items: center;
gap: var(--space-2);
cursor: pointer;
}
.selected-path {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-dim);
padding: var(--space-1) var(--space-2);
background: var(--surface-2);
border-radius: 3px;
word-break: break-all;
}
.run-btn {
background: var(--status-live);
color: #000;
border: none;
border-radius: 4px;
padding: var(--space-2) var(--space-3);
font-family: var(--font-mono);
font-size: var(--font-size-sm);
font-weight: 600;
cursor: pointer;
}
.run-btn:hover { opacity: 0.9; }
.run-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.source-actions {
flex-shrink: 0;
display: flex;
justify-content: flex-end;
margin-top: auto;
}
.editor-close {
background: var(--surface-3);
border: 1px solid var(--surface-3);
border-radius: 4px;
padding: var(--space-2) var(--space-3);
color: var(--text-secondary);
font-family: var(--font-mono);
font-size: var(--font-size-sm);
cursor: pointer;
}
.editor-close:hover {
background: var(--status-error);
color: #000;
}
</style>