feat: implement page components for ShadowRun and Models features

Add 4 Vue 3 pages with KBX adapter integration:

Pages:
- ShadowRunList.vue (252+ day validation search & grid)
- ShadowRunDetail.vue (metrics breakdown, phase analysis)
- ModelsList.vue (lifecycle management, quick filters)
- ModelDetail.vue (activation requirements, configuration)

Features:
- Registry-driven screen definitions
- KbxListPage + KbxDataGrid + KbxButton adapters
- Mock data (replaced with TanStack Query in Task F)
- Keyboard shortcuts (F3, Ctrl+N, Escape, Ctrl+E)
- Responsive density-aware layout
- Validation indicators (PBO, DSR, OOS thresholds)
- Phase lifecycle visualization
- Quick filter badges

Implementation pattern:
1. useKbxRegistry() for screen access
2. Computed state for data state management
3. useRoute/useRouter for navigation
4. Slots for flexible layout composition

Ready for Task F: TanStack Query API integration

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 01:43:43 +09:00
parent c41e5063b7
commit d29f0e7df9
4 changed files with 1574 additions and 0 deletions
@@ -0,0 +1,264 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import KbxListPage from '@shared/ui/adapter/KbxListPage.vue'
import KbxDataGrid from '@shared/ui/adapter/KbxDataGrid.vue'
import KbxButton from '@shared/ui/adapter/KbxButton.vue'
import KbxInput from '@shared/ui/adapter/KbxInput.vue'
import { useKbxRegistry } from '@shared/composables/useKbxRegistry'
const route = useRoute()
const router = useRouter()
const registry = useKbxRegistry()
// Get screen definition from registry
const screenDef = computed(() =>
registry.getScreen('model-ops.models.list'),
)
// Search and filter state
const searchQuery = ref('')
const phaseFilter = ref('all')
const activeFilter = ref('all')
// Pagination
const currentPage = ref(1)
const pageSize = ref(50)
// Mock data state
const isLoading = ref(false)
const error = ref<string | null>(null)
const dataState = computed<'idle' | 'pending' | 'ready' | 'error' | 'empty'>(() => {
if (isLoading.value) return 'pending'
if (error.value) return 'error'
if (models.value.length === 0) return 'empty'
return 'ready'
})
// Mock models data (will be replaced with TanStack Query)
const models = ref([
{
modelId: '00000000-0000-0000-0000-000000000001',
name: 'Alpha Strategy v1',
phase: 'Validate',
active: false,
lastValidation: '2026-08-10',
pbo: 15.2,
dsr: 96.5,
returnMtd: 12.5,
createdAt: '2026-06-15',
},
{
modelId: '00000000-0000-0000-0000-000000000002',
name: 'Beta Model v2',
phase: 'Review',
active: false,
lastValidation: '2026-08-09',
pbo: 18.3,
dsr: 94.2,
returnMtd: 8.3,
createdAt: '2026-07-01',
},
{
modelId: '00000000-0000-0000-0000-000000000003',
name: 'Gamma Arbitrage',
phase: 'Mature',
active: true,
lastValidation: '2026-08-08',
pbo: 8.5,
dsr: 98.1,
returnMtd: 18.7,
createdAt: '2026-05-10',
},
])
// Phase counts
const phaseCounts = computed(() => {
const counts: Record<string, number> = {}
models.value.forEach(m => {
counts[m.phase] = (counts[m.phase] || 0) + 1
})
return counts
})
// Quick filters
const quickFilters = computed(() => [
{ id: 'all', label: 'All', active: phaseFilter.value === 'all', badge: models.value.length },
{ id: 'active', label: 'Active', active: activeFilter.value === 'active', badge: models.value.filter(m => m.active).length },
{ id: 'ready', label: 'Ready to Deploy', active: phaseFilter.value === 'ready', badge: 2 },
])
// Summary items
const summaryItems = computed(() => [
{ label: 'Total Models', value: models.value.length },
{ label: 'Active', value: models.value.filter(m => m.active).length },
{ label: 'Ready to Deploy', value: 2 },
{ label: 'Avg PBO', value: (models.value.reduce((sum, m) => sum + m.pbo, 0) / models.value.length).toFixed(1) },
])
// Actions
const handleSearch = () => {
isLoading.value = true
setTimeout(() => {
isLoading.value = false
}, 500)
}
const handleNewModel = () => {
router.push('/model-ops/models/new')
}
const handleRowClick = (modelId: string) => {
router.push(`/model-ops/models/${modelId}`)
}
const handleQuickFilter = (filterId: string) => {
if (filterId === 'active') {
activeFilter.value = activeFilter.value === 'active' ? 'all' : 'active'
} else {
phaseFilter.value = filterId
}
}
const handleRefresh = () => {
handleSearch()
}
// Keyboard shortcuts
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'F3') {
e.preventDefault()
handleSearch()
} else if (e.ctrlKey && e.key === 'n') {
e.preventDefault()
handleNewModel()
}
}
import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
window.addEventListener('keydown', handleKeydown)
})
onUnmounted(() => {
window.removeEventListener('keydown', handleKeydown)
})
</script>
<template>
<div v-if="screenDef" class="models-list">
<KbxListPage
:screen="screenDef"
:data-state="dataState"
:loading="isLoading"
:summary-items="summaryItems"
:quick-filters="quickFilters"
@quick-filter="handleQuickFilter"
@refresh="handleRefresh"
>
<!-- Header Actions -->
<template #header-actions>
<KbxButton
label="New Model"
variant="primary"
@click="handleNewModel"
/>
</template>
<!-- Search Panel -->
<template #search>
<div class="models-search">
<div class="search-row">
<KbxInput
v-model="searchQuery"
placeholder="Search by model name..."
@keydown.enter="handleSearch"
/>
<KbxButton
label="Search"
variant="default"
@click="handleSearch"
/>
</div>
<div class="search-row">
<select v-model="phaseFilter" class="phase-filter">
<option value="all">All Phases</option>
<option value="freeze">Freeze</option>
<option value="mature">Mature</option>
<option value="score">Score</option>
<option value="diagnose">Diagnose</option>
<option value="hypothesis">Hypothesis</option>
<option value="challenger">Challenger</option>
<option value="validate">Validate</option>
<option value="review">Review</option>
</select>
<select v-model="activeFilter" class="active-filter">
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
</div>
</template>
<!-- Content Area -->
<template #content>
<KbxDataGrid
v-if="screenDef.grid"
:columns="screenDef.grid.columnDefs"
:rows="models"
:loading="isLoading"
@row-click="(modelId) => handleRowClick(modelId)"
/>
</template>
</KbxListPage>
</div>
</template>
<style scoped>
.models-list {
display: flex;
flex-direction: column;
height: 100%;
}
.models-search {
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px;
background: var(--kbx-color-surface, #f5f5f5);
border-radius: 4px;
}
.search-row {
display: flex;
gap: 12px;
align-items: center;
}
.search-row input,
.search-row select {
height: var(--kbx-input-height, 34px);
padding: 4px 8px;
border: 1px solid #d0d0d0;
border-radius: 4px;
font-size: var(--kbx-font-size, 14px);
}
.phase-filter,
.active-filter {
flex: 0 0 140px;
}
.badge {
background: var(--kbx-color-primary, #3b82f6);
color: white;
padding: 2px 6px;
border-radius: 12px;
font-size: 11px;
margin-left: 4px;
}
</style>