feat: add run deletion, session reopen, and feed merging UI

This commit is contained in:
Jan-Peter von Hunnius 2026-06-06 10:20:43 +02:00
parent 3fa5a1441a
commit a6af6e4b98
4 changed files with 107 additions and 0 deletions

View file

@ -88,6 +88,15 @@ export async function stopRun(sessionId: string, runId: string): Promise<Session
return res.data return res.data
} }
export async function deleteRun(sessionId: string, runId: string): Promise<void> {
await api.delete(`/sessions/${sessionId}/runs/${runId}`)
}
export async function reopenSession(id: string): Promise<UseCaseSession> {
const res = await api.post<UseCaseSession>(`/sessions/${id}/reopen`)
return res.data
}
export async function completeSession(id: string): Promise<UseCaseSession> { export async function completeSession(id: string): Promise<UseCaseSession> {
const res = await api.post<UseCaseSession>(`/sessions/${id}/complete`) const res = await api.post<UseCaseSession>(`/sessions/${id}/complete`)
return res.data return res.data

View file

@ -119,6 +119,40 @@ export const useSessionsStore = defineStore('sessions', () => {
} }
} }
async function deleteRun(sessionId: string, runId: string): Promise<boolean> {
try {
await api.deleteRun(sessionId, runId)
if (currentSession.value?.id === sessionId) {
currentSession.value.runs = currentSession.value.runs.filter((r) => r.id !== runId)
const sIdx = sessions.value.findIndex((s) => s.id === sessionId)
if (sIdx !== -1) {
sessions.value[sIdx] = { ...sessions.value[sIdx], runs: [...currentSession.value.runs] }
}
}
return true
} catch (e: unknown) {
error.value = e instanceof Error ? e.message : 'Failed to delete run'
return false
}
}
async function reopenSession(id: string): Promise<UseCaseSession | null> {
try {
const session = await api.reopenSession(id)
const idx = sessions.value.findIndex((s) => s.id === id)
if (idx !== -1) {
sessions.value[idx] = session
}
if (currentSession.value?.id === id) {
currentSession.value = session
}
return session
} catch (e: unknown) {
error.value = e instanceof Error ? e.message : 'Failed to reopen session'
return null
}
}
return { return {
sessions, sessions,
currentSession, currentSession,
@ -130,7 +164,9 @@ export const useSessionsStore = defineStore('sessions', () => {
deleteSession, deleteSession,
startRun, startRun,
stopRun, stopRun,
deleteRun,
completeSession, completeSession,
reopenSession,
reAnalyze, reAnalyze,
} }
}) })

View file

@ -112,6 +112,7 @@ export interface CreateSessionRequest {
export interface StartRunRequest { export interface StartRunRequest {
feed_name: string feed_name: string
label?: string | null label?: string | null
merge_feeds?: string[]
} }
// ── Use Cases ── // ── Use Cases ──

View file

@ -16,12 +16,18 @@ const store = useSessionsStore()
const showStartRun = ref(false) const showStartRun = ref(false)
const runFeedName = ref('ksudp') const runFeedName = ref('ksudp')
const runLabel = ref('') const runLabel = ref('')
const runMergeEnabled = ref(false)
const runMergeFeeds = ref<string[]>([])
const sessionId = computed(() => props.id ?? (route.params.id as string)) const sessionId = computed(() => props.id ?? (route.params.id as string))
const session = computed(() => store.currentSession) const session = computed(() => store.currentSession)
const feedOptions = ['ksudp', 'telemetry_tool', 'pcars'] const feedOptions = ['ksudp', 'telemetry_tool', 'pcars']
const mergeFeedOptions = computed(() => {
return feedOptions.filter((f) => f !== runFeedName.value)
})
const hasActiveRun = computed(() => { const hasActiveRun = computed(() => {
return session.value?.runs.some((r) => r.status === 'recording') ?? false return session.value?.runs.some((r) => r.status === 'recording') ?? false
}) })
@ -61,10 +67,13 @@ async function handleStartRun() {
const run = await store.startRun(sessionId.value, { const run = await store.startRun(sessionId.value, {
feed_name: runFeedName.value, feed_name: runFeedName.value,
label: runLabel.value || undefined, label: runLabel.value || undefined,
merge_feeds: runMergeEnabled.value && runMergeFeeds.value.length > 0 ? runMergeFeeds.value : undefined,
}) })
if (run) { if (run) {
showStartRun.value = false showStartRun.value = false
runLabel.value = '' runLabel.value = ''
runMergeEnabled.value = false
runMergeFeeds.value = []
} }
} }
@ -102,6 +111,16 @@ async function handleDelete() {
router.push('/sessions') router.push('/sessions')
} }
} }
async function handleDeleteRun(runId: string, runLabel: string) {
if (confirm(`Delete run "${runLabel || 'Untitled Run'}"?`)) {
await store.deleteRun(sessionId.value, runId)
}
}
async function handleReopen() {
await store.reopenSession(sessionId.value)
}
</script> </script>
<template> <template>
@ -181,6 +200,18 @@ async function handleDelete() {
/> />
<button class="btn btn-primary btn-sm" @click="handleStartRun">Start</button> <button class="btn btn-primary btn-sm" @click="handleStartRun">Start</button>
</div> </div>
<div class="flex items-center gap-2 mt-2">
<label class="merge-toggle">
<input type="checkbox" v-model="runMergeEnabled" />
<span>Merge with additional feeds</span>
</label>
</div>
<div v-if="runMergeEnabled" class="flex gap-2 items-center mt-1">
<label v-for="f in mergeFeedOptions" :key="f" class="merge-feed-option">
<input type="checkbox" :value="f" v-model="runMergeFeeds" />
<span>{{ f }}</span>
</label>
</div>
</div> </div>
<div v-if="session.runs.length === 0" class="text-secondary text-muted"> <div v-if="session.runs.length === 0" class="text-secondary text-muted">
@ -208,6 +239,12 @@ async function handleDelete() {
> >
■ Stop ■ Stop
</button> </button>
<button
class="btn btn-danger btn-sm"
@click="handleDeleteRun(run.id, run.label || '')"
>
✕
</button>
</div> </div>
</div> </div>
</div> </div>
@ -279,6 +316,9 @@ async function handleDelete() {
<button class="btn btn-secondary" @click="handleReAnalyze"> <button class="btn btn-secondary" @click="handleReAnalyze">
🔄 Re-analyze 🔄 Re-analyze
</button> </button>
<button class="btn btn-secondary" @click="handleReopen">
↩ Reopen Session
</button>
</div> </div>
</div> </div>
@ -385,4 +425,25 @@ async function handleDelete() {
flex: 1; flex: 1;
min-width: 150px; min-width: 150px;
} }
.merge-toggle {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.85rem;
color: var(--color-text-secondary);
cursor: pointer;
}
.merge-feed-option {
display: flex;
align-items: center;
gap: 0.3rem;
font-size: 0.8rem;
color: var(--color-text-secondary);
padding: 0.2rem 0.5rem;
background: var(--color-background);
border-radius: var(--radius-sm);
cursor: pointer;
}
</style> </style>