home-lab/research/Vue-Nuxt-Web-Interface-Design.md
2025-06-16 13:43:21 +02:00

1536 lines
42 KiB
Markdown

# Vue/Nuxt Web Interface Design for RAG + MCP + TaskMaster
## Executive Summary
This document outlines the design for a modern Vue/Nuxt web interface that integrates with the existing RAG + MCP + TaskMaster domain model and event architecture. The web interface provides a centralized dashboard for managing projects, monitoring infrastructure, interacting with AI systems, and exploring knowledge bases.
**Key Features:**
- Real-time project and task management
- Infrastructure monitoring and control
- RAG system interaction and query interface
- MCP server management
- Event-driven updates and notifications
- Responsive, modern UI with TypeScript support
## Table of Contents
1. [Architecture Overview](#architecture-overview)
2. [Component Architecture](#component-architecture)
3. [API Integration Strategy](#api-integration-strategy)
4. [Real-time Data Flow](#real-time-data-flow)
5. [Page Structure](#page-structure)
6. [State Management](#state-management)
7. [Authentication & Security](#authentication--security)
8. [Implementation Plan](#implementation-plan)
## Architecture Overview
### Technology Stack
```mermaid
graph TB
subgraph "Frontend Layer"
A[Nuxt 3] --> B[Vue 3 Composition API]
B --> C[TypeScript]
C --> D[Tailwind CSS / Nuxt UI]
D --> E[VueUse Composables]
end
subgraph "API Layer"
F[Nuxt Server API] --> G[h3 Event Handlers]
G --> H[Runtime Config]
H --> I[Request Validation]
end
subgraph "Integration Layer"
J[Python RAG Service] --> K[FastAPI/Guile Bridge]
K --> L[MCP Server]
L --> M[TaskMaster CLI/API]
end
subgraph "Real-time Layer"
N[WebSocket/SSE] --> O[Event Bus]
O --> P[Reactive State]
P --> Q[Component Updates]
end
```
### Directory Structure
```
nuxt-web-interface/
├── components/
│ ├── dashboard/
│ │ ├── ProjectOverview.vue
│ │ ├── TaskBoard.vue
│ │ ├── InfrastructureStatus.vue
│ │ └── ActivityFeed.vue
│ ├── project/
│ │ ├── ProjectCard.vue
│ │ ├── PhaseTimeline.vue
│ │ ├── TaskList.vue
│ │ └── DeliverableTracker.vue
│ ├── infrastructure/
│ │ ├── ServiceGrid.vue
│ │ ├── DeploymentStatus.vue
│ │ ├── LogViewer.vue
│ │ └── MetricsChart.vue
│ ├── rag/
│ │ ├── QueryInterface.vue
│ │ ├── DocumentExplorer.vue
│ │ ├── EmbeddingVisualizer.vue
│ │ └── ResultsPanel.vue
│ ├── mcp/
│ │ ├── ServerStatus.vue
│ │ ├── ToolRegistry.vue
│ │ ├── ClientConnections.vue
│ │ └── CapabilityMatrix.vue
│ └── ui/
│ ├── LoadingSpinner.vue
│ ├── NotificationToast.vue
│ ├── ConfirmDialog.vue
│ └── StatusBadge.vue
├── composables/
│ ├── useProject.ts
│ ├── useInfrastructure.ts
│ ├── useRAG.ts
│ ├── useMCP.ts
│ ├── useWebSocket.ts
│ ├── useEventBus.ts
│ └── useNotifications.ts
├── pages/
│ ├── index.vue
│ ├── projects/
│ │ ├── index.vue
│ │ ├── [id].vue
│ │ └── new.vue
│ ├── infrastructure/
│ │ ├── index.vue
│ │ ├── services.vue
│ │ └── deployments.vue
│ ├── rag/
│ │ ├── index.vue
│ │ ├── query.vue
│ │ └── documents.vue
│ ├── mcp/
│ │ ├── index.vue
│ │ ├── servers.vue
│ │ └── tools.vue
│ └── settings/
│ ├── index.vue
│ └── integrations.vue
├── server/
│ └── api/
│ ├── projects/
│ │ ├── index.get.ts
│ │ ├── index.post.ts
│ │ ├── [id].get.ts
│ │ └── [id].patch.ts
│ ├── infrastructure/
│ │ ├── services.get.ts
│ │ └── deployments.get.ts
│ ├── rag/
│ │ ├── query.post.ts
│ │ ├── documents.get.ts
│ │ └── embeddings.get.ts
│ ├── mcp/
│ │ ├── servers.get.ts
│ │ └── tools.get.ts
│ └── events/
│ └── stream.get.ts
├── types/
│ ├── domain.ts
│ ├── api.ts
│ └── events.ts
└── nuxt.config.ts
```
## Component Architecture
### Core Composition API Patterns
#### Project Management Components
```vue
<!-- components/project/ProjectCard.vue -->
<script setup lang="ts">
import type { Project } from '~/types/domain'
interface Props {
project: Project
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:status': [status: ProjectStatus]
'view:details': [projectId: string]
}>()
// Reactive state
const isExpanded = ref(false)
const { updateProjectStatus } = useProject()
// Computed properties
const statusColor = computed(() => {
switch (props.project.status) {
case 'completed': return 'green'
case 'in_progress': return 'blue'
case 'paused': return 'yellow'
case 'cancelled': return 'red'
default: return 'gray'
}
})
const completionPercentage = computed(() => {
if (!props.project.phases.length) return 0
const completedPhases = props.project.phases.filter(p => p.status === 'completed').length
return Math.round((completedPhases / props.project.phases.length) * 100)
})
// Event handlers
const handleStatusUpdate = async (newStatus: ProjectStatus) => {
try {
await updateProjectStatus(props.project.id, newStatus)
emit('update:status', newStatus)
} catch (error) {
// Handle error with notification
console.error('Failed to update project status:', error)
}
}
const viewDetails = () => {
emit('view:details', props.project.id)
}
</script>
<template>
<div class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
<div class="flex justify-between items-start mb-4">
<div>
<h3 class="text-lg font-semibold text-gray-900">{{ project.name }}</h3>
<p class="text-sm text-gray-600 mt-1">{{ project.description }}</p>
</div>
<StatusBadge :status="project.status" :color="statusColor" />
</div>
<div class="mb-4">
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>Progress</span>
<span>{{ completionPercentage }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
:style="{ width: `${completionPercentage}%` }"
/>
</div>
</div>
<div class="flex justify-between items-center">
<div class="text-sm text-gray-500">
{{ project.phases.length }} phases
</div>
<div class="space-x-2">
<button
@click="isExpanded = !isExpanded"
class="text-blue-600 hover:text-blue-800 text-sm"
>
{{ isExpanded ? 'Collapse' : 'Expand' }}
</button>
<button
@click="viewDetails"
class="bg-blue-600 text-white px-3 py-1 rounded text-sm hover:bg-blue-700"
>
View Details
</button>
</div>
</div>
<div v-if="isExpanded" class="mt-4 pt-4 border-t border-gray-200">
<PhaseTimeline :phases="project.phases" @update:phase="handlePhaseUpdate" />
</div>
</div>
</template>
```
#### RAG Query Interface Component
```vue
<!-- components/rag/QueryInterface.vue -->
<script setup lang="ts">
import type { RAGQuery, RAGResult } from '~/types/domain'
// Reactive state
const query = ref('')
const isQuerying = ref(false)
const results = ref<RAGResult[]>([])
const queryHistory = ref<RAGQuery[]>([])
// Composables
const { executeQuery, getQueryHistory } = useRAG()
const { addNotification } = useNotifications()
// Load query history on mount
onMounted(async () => {
try {
queryHistory.value = await getQueryHistory()
} catch (error) {
console.error('Failed to load query history:', error)
}
})
// Execute RAG query
const submitQuery = async () => {
if (!query.value.trim()) return
isQuerying.value = true
try {
const result = await executeQuery(query.value)
results.value = result.documents
// Add to history
queryHistory.value.unshift({
id: Date.now().toString(),
query: query.value,
timestamp: new Date(),
resultCount: result.documents.length
})
addNotification({
type: 'success',
title: 'Query completed',
message: `Found ${result.documents.length} relevant documents`
})
} catch (error) {
console.error('Query failed:', error)
addNotification({
type: 'error',
title: 'Query failed',
message: 'Please try again or check the RAG system status'
})
} finally {
isQuerying.value = false
}
}
// Handle query from history
const selectHistoryQuery = (historicQuery: RAGQuery) => {
query.value = historicQuery.query
}
// Clear results
const clearResults = () => {
results.value = []
query.value = ''
}
</script>
<template>
<div class="space-y-6">
<!-- Query Input -->
<div class="bg-white rounded-lg shadow p-6">
<h2 class="text-xl font-semibold mb-4">RAG Query Interface</h2>
<div class="space-y-4">
<div>
<label for="query" class="block text-sm font-medium text-gray-700 mb-2">
Enter your query
</label>
<textarea
id="query"
v-model="query"
rows="3"
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
placeholder="Ask a question about your documents..."
@keydown.ctrl.enter="submitQuery"
/>
</div>
<div class="flex justify-between items-center">
<div class="text-sm text-gray-500">
Press Ctrl+Enter to submit
</div>
<div class="space-x-2">
<button
@click="clearResults"
class="px-4 py-2 text-gray-600 border border-gray-300 rounded hover:bg-gray-50"
:disabled="!results.length && !query"
>
Clear
</button>
<button
@click="submitQuery"
:disabled="!query.trim() || isQuerying"
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
<LoadingSpinner v-if="isQuerying" class="inline w-4 h-4 mr-2" />
{{ isQuerying ? 'Querying...' : 'Submit Query' }}
</button>
</div>
</div>
</div>
</div>
<!-- Query History -->
<div v-if="queryHistory.length" class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-medium mb-4">Recent Queries</h3>
<div class="space-y-2 max-h-40 overflow-y-auto">
<div
v-for="historicQuery in queryHistory.slice(0, 5)"
:key="historicQuery.id"
class="p-2 bg-gray-50 rounded cursor-pointer hover:bg-gray-100"
@click="selectHistoryQuery(historicQuery)"
>
<div class="text-sm font-medium truncate">{{ historicQuery.query }}</div>
<div class="text-xs text-gray-500">
{{ new Date(historicQuery.timestamp).toLocaleString() }}
{{ historicQuery.resultCount }} results
</div>
</div>
</div>
</div>
<!-- Results Panel -->
<ResultsPanel v-if="results.length" :results="results" />
</div>
</template>
```
### Composables Architecture
#### Project Management Composable
```typescript
// composables/useProject.ts
import type { Project, ProjectStatus, Phase, Task } from '~/types/domain'
export const useProject = () => {
const projects = ref<Project[]>([])
const currentProject = ref<Project | null>(null)
const loading = ref(false)
const error = ref<string | null>(null)
// Fetch all projects
const fetchProjects = async (): Promise<void> => {
loading.value = true
error.value = null
try {
const { data } = await $fetch<{ projects: Project[] }>('/api/projects')
projects.value = data.projects
} catch (err) {
error.value = 'Failed to fetch projects'
console.error('Error fetching projects:', err)
} finally {
loading.value = false
}
}
// Fetch single project
const fetchProject = async (id: string): Promise<Project | null> => {
loading.value = true
error.value = null
try {
const { data } = await $fetch<{ project: Project }>(`/api/projects/${id}`)
currentProject.value = data.project
return data.project
} catch (err) {
error.value = 'Failed to fetch project'
console.error('Error fetching project:', err)
return null
} finally {
loading.value = false
}
}
// Create new project
const createProject = async (projectData: Omit<Project, 'id'>): Promise<Project | null> => {
loading.value = true
error.value = null
try {
const { data } = await $fetch<{ project: Project }>('/api/projects', {
method: 'POST',
body: projectData
})
projects.value.push(data.project)
return data.project
} catch (err) {
error.value = 'Failed to create project'
console.error('Error creating project:', err)
return null
} finally {
loading.value = false
}
}
// Update project status
const updateProjectStatus = async (id: string, status: ProjectStatus): Promise<boolean> => {
try {
await $fetch(`/api/projects/${id}`, {
method: 'PATCH',
body: { status }
})
// Update local state
const projectIndex = projects.value.findIndex(p => p.id === id)
if (projectIndex !== -1) {
projects.value[projectIndex].status = status
}
if (currentProject.value?.id === id) {
currentProject.value.status = status
}
return true
} catch (err) {
error.value = 'Failed to update project status'
console.error('Error updating project status:', err)
return false
}
}
// Add new task to project
const addTask = async (projectId: string, phaseId: string, taskData: Omit<Task, 'id'>): Promise<Task | null> => {
try {
const { data } = await $fetch<{ task: Task }>(`/api/projects/${projectId}/phases/${phaseId}/tasks`, {
method: 'POST',
body: taskData
})
// Update local state
const project = projects.value.find(p => p.id === projectId)
if (project) {
const phase = project.phases.find(ph => ph.id === phaseId)
if (phase) {
phase.tasks.push(data.task)
}
}
return data.task
} catch (err) {
error.value = 'Failed to add task'
console.error('Error adding task:', err)
return null
}
}
// Computed properties
const activeProjects = computed(() =>
projects.value.filter(p => p.status === 'in_progress')
)
const completedProjects = computed(() =>
projects.value.filter(p => p.status === 'completed')
)
const projectStats = computed(() => ({
total: projects.value.length,
active: activeProjects.value.length,
completed: completedProjects.value.length,
planned: projects.value.filter(p => p.status === 'planned').length
}))
return {
// State
projects: readonly(projects),
currentProject: readonly(currentProject),
loading: readonly(loading),
error: readonly(error),
// Actions
fetchProjects,
fetchProject,
createProject,
updateProjectStatus,
addTask,
// Computed
activeProjects,
completedProjects,
projectStats
}
}
```
#### Real-time Events Composable
```typescript
// composables/useEventBus.ts
import type { DomainEvent } from '~/types/events'
export const useEventBus = () => {
const eventSource = ref<EventSource | null>(null)
const connected = ref(false)
const events = ref<DomainEvent[]>([])
const lastEventId = ref<string | null>(null)
// Event listeners
const listeners = new Map<string, Set<(event: DomainEvent) => void>>()
// Connect to server-sent events
const connect = (): void => {
if (eventSource.value) return
eventSource.value = new EventSource('/api/events/stream')
eventSource.value.onopen = () => {
connected.value = true
console.log('Event stream connected')
}
eventSource.value.onmessage = (event) => {
try {
const domainEvent: DomainEvent = JSON.parse(event.data)
events.value.unshift(domainEvent)
lastEventId.value = domainEvent.id
// Emit to specific listeners
const typeListeners = listeners.get(domainEvent.type)
if (typeListeners) {
typeListeners.forEach(listener => listener(domainEvent))
}
// Emit to wildcard listeners
const wildcardListeners = listeners.get('*')
if (wildcardListeners) {
wildcardListeners.forEach(listener => listener(domainEvent))
}
} catch (error) {
console.error('Failed to parse event:', error)
}
}
eventSource.value.onerror = () => {
connected.value = false
console.error('Event stream error')
}
}
// Disconnect from events
const disconnect = (): void => {
if (eventSource.value) {
eventSource.value.close()
eventSource.value = null
}
connected.value = false
}
// Subscribe to specific event types
const subscribe = (eventType: string, callback: (event: DomainEvent) => void): () => void => {
if (!listeners.has(eventType)) {
listeners.set(eventType, new Set())
}
listeners.get(eventType)!.add(callback)
// Return unsubscribe function
return () => {
const typeListeners = listeners.get(eventType)
if (typeListeners) {
typeListeners.delete(callback)
if (typeListeners.size === 0) {
listeners.delete(eventType)
}
}
}
}
// Get events by type
const getEventsByType = (eventType: string): DomainEvent[] => {
return events.value.filter(event => event.type === eventType)
}
// Auto-connect on mount, disconnect on unmount
onMounted(() => {
connect()
})
onUnmounted(() => {
disconnect()
})
return {
// State
connected: readonly(connected),
events: readonly(events),
lastEventId: readonly(lastEventId),
// Actions
connect,
disconnect,
subscribe,
getEventsByType
}
}
```
## API Integration Strategy
### Server API Routes
#### Project Management API
```typescript
// server/api/projects/index.get.ts
import type { Project } from '~/types/domain'
export default defineEventHandler(async (event): Promise<{ projects: Project[] }> => {
try {
// Get TaskMaster configuration
const config = useRuntimeConfig(event)
// Call TaskMaster via CLI or direct API
const response = await $fetch(`${config.taskMasterUrl}/api/projects`, {
headers: {
'Authorization': `Bearer ${config.taskMasterApiKey}`
}
})
return { projects: response.data }
} catch (error) {
console.error('Failed to fetch projects:', error)
throw createError({
statusCode: 500,
statusMessage: 'Failed to fetch projects from TaskMaster'
})
}
})
```
```typescript
// server/api/projects/[id].patch.ts
export default defineEventHandler(async (event) => {
const projectId = getRouterParam(event, 'id')
const body = await readBody(event)
if (!projectId) {
throw createError({
statusCode: 400,
statusMessage: 'Project ID is required'
})
}
try {
const config = useRuntimeConfig(event)
// Update project via TaskMaster API
const response = await $fetch(`${config.taskMasterUrl}/api/projects/${projectId}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${config.taskMasterApiKey}`,
'Content-Type': 'application/json'
},
body
})
return { project: response.data }
} catch (error) {
console.error('Failed to update project:', error)
throw createError({
statusCode: 500,
statusMessage: 'Failed to update project'
})
}
})
```
#### RAG System API
```typescript
// server/api/rag/query.post.ts
export default defineEventHandler(async (event) => {
const { query, options = {} } = await readBody(event)
if (!query || typeof query !== 'string') {
throw createError({
statusCode: 400,
statusMessage: 'Query is required and must be a string'
})
}
try {
const config = useRuntimeConfig(event)
// Call Python RAG service
const response = await $fetch(`${config.ragServiceUrl}/query`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${config.ragServiceApiKey}`,
'Content-Type': 'application/json'
},
body: {
query,
...options
}
})
return response
} catch (error) {
console.error('RAG query failed:', error)
throw createError({
statusCode: 500,
statusMessage: 'RAG query failed'
})
}
})
```
#### Event Stream API
```typescript
// server/api/events/stream.get.ts
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig(event)
// Set up Server-Sent Events
setHeader(event, 'Content-Type', 'text/event-stream')
setHeader(event, 'Cache-Control', 'no-cache')
setHeader(event, 'Connection', 'keep-alive')
setHeader(event, 'Access-Control-Allow-Origin', '*')
// Connect to event source (Redis, EventStore, or direct connection)
try {
// This would connect to your event store
const eventStream = await connectToEventStore(config.eventStoreUrl)
eventStream.on('event', (domainEvent) => {
event.node.res.write(`data: ${JSON.stringify(domainEvent)}\n\n`)
})
// Keep connection alive
const keepAlive = setInterval(() => {
event.node.res.write(': keep-alive\n\n')
}, 30000)
// Clean up on close
event.node.req.on('close', () => {
clearInterval(keepAlive)
eventStream.close()
})
} catch (error) {
console.error('Failed to connect to event stream:', error)
throw createError({
statusCode: 500,
statusMessage: 'Failed to establish event stream'
})
}
})
```
## Page Structure
### Main Dashboard
```vue
<!-- pages/index.vue -->
<script setup lang="ts">
// SEO
useHead({
title: 'RAG + MCP + TaskMaster Dashboard',
meta: [
{ name: 'description', content: 'Home lab management dashboard for RAG, MCP, and TaskMaster integration' }
]
})
// Composables
const { projects, fetchProjects, projectStats } = useProject()
const { services, fetchServices } = useInfrastructure()
const { systemStatus } = useRAG()
const { servers } = useMCP()
const { subscribe } = useEventBus()
// Reactive state
const dashboardData = ref({
recentActivity: [],
alerts: [],
systemHealth: 'healthy'
})
// Fetch initial data
onMounted(async () => {
await Promise.all([
fetchProjects(),
fetchServices()
])
})
// Subscribe to real-time events
const unsubscribe = subscribe('*', (event) => {
// Add to recent activity
dashboardData.value.recentActivity.unshift({
id: event.id,
type: event.type,
message: event.summary,
timestamp: event.timestamp
})
// Keep only last 20 activities
if (dashboardData.value.recentActivity.length > 20) {
dashboardData.value.recentActivity = dashboardData.value.recentActivity.slice(0, 20)
}
})
onUnmounted(() => {
unsubscribe()
})
</script>
<template>
<div class="space-y-6">
<!-- Header -->
<div class="bg-white shadow">
<div class="px-6 py-4">
<h1 class="text-2xl font-bold text-gray-900">
RAG + MCP + TaskMaster Dashboard
</h1>
<p class="text-gray-600 mt-1">
Monitor and manage your home lab infrastructure and AI systems
</p>
</div>
</div>
<!-- Stats Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-blue-100 rounded-lg">
<Icon name="heroicons:folder" class="w-6 h-6 text-blue-600" />
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Active Projects</p>
<p class="text-2xl font-semibold text-gray-900">{{ projectStats.active }}</p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-green-100 rounded-lg">
<Icon name="heroicons:server" class="w-6 h-6 text-green-600" />
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Running Services</p>
<p class="text-2xl font-semibold text-gray-900">{{ services.filter(s => s.status === 'running').length }}</p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-purple-100 rounded-lg">
<Icon name="heroicons:cpu-chip" class="w-6 h-6 text-purple-600" />
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">RAG Status</p>
<p class="text-2xl font-semibold text-gray-900">{{ systemStatus }}</p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-orange-100 rounded-lg">
<Icon name="heroicons:cog-8-tooth" class="w-6 h-6 text-orange-600" />
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">MCP Servers</p>
<p class="text-2xl font-semibold text-gray-900">{{ servers.filter(s => s.status === 'running').length }}</p>
</div>
</div>
</div>
</div>
<!-- Main Content Grid -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Project Overview -->
<div class="lg:col-span-2">
<ProjectOverview :projects="projects" />
</div>
<!-- Activity Feed -->
<div>
<ActivityFeed :activities="dashboardData.recentActivity" />
</div>
<!-- Infrastructure Status -->
<div class="lg:col-span-2">
<InfrastructureStatus :services="services" />
</div>
<!-- Quick Actions -->
<div>
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-medium mb-4">Quick Actions</h3>
<div class="space-y-3">
<NuxtLink
to="/rag/query"
class="block w-full bg-blue-600 text-white text-center py-2 rounded hover:bg-blue-700"
>
Query RAG System
</NuxtLink>
<NuxtLink
to="/projects/new"
class="block w-full bg-green-600 text-white text-center py-2 rounded hover:bg-green-700"
>
New Project
</NuxtLink>
<NuxtLink
to="/infrastructure"
class="block w-full bg-purple-600 text-white text-center py-2 rounded hover:bg-purple-700"
>
Manage Infrastructure
</NuxtLink>
</div>
</div>
</div>
</div>
</div>
</template>
```
### Project Detail Page
```vue
<!-- pages/projects/[id].vue -->
<script setup lang="ts">
const route = useRoute()
const router = useRouter()
// Fetch project data
const { currentProject, fetchProject, updateProjectStatus } = useProject()
const { subscribe } = useEventBus()
const projectId = route.params.id as string
// Reactive state
const activeTab = ref('overview')
const showEditDialog = ref(false)
// Fetch project on mount
onMounted(async () => {
const project = await fetchProject(projectId)
if (!project) {
throw createError({
statusCode: 404,
statusMessage: 'Project not found'
})
}
})
// Subscribe to project events
const unsubscribe = subscribe('project.*', (event) => {
if (event.aggregateId === projectId) {
// Refresh project data when project events occur
fetchProject(projectId)
}
})
onUnmounted(() => {
unsubscribe()
})
// SEO
useHead(() => ({
title: currentProject.value ? `${currentProject.value.name} - Project Details` : 'Project Details',
meta: [
{ name: 'description', content: currentProject.value?.description || 'Project details' }
]
}))
</script>
<template>
<div v-if="currentProject" class="space-y-6">
<!-- Header -->
<div class="bg-white shadow">
<div class="px-6 py-4">
<div class="flex justify-between items-start">
<div>
<div class="flex items-center space-x-2 mb-2">
<button
@click="router.back()"
class="text-gray-500 hover:text-gray-700"
>
<Icon name="heroicons:arrow-left" class="w-5 h-5" />
</button>
<h1 class="text-2xl font-bold text-gray-900">{{ currentProject.name }}</h1>
<StatusBadge :status="currentProject.status" />
</div>
<p class="text-gray-600">{{ currentProject.description }}</p>
</div>
<div class="space-x-2">
<button
@click="showEditDialog = true"
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
>
Edit Project
</button>
</div>
</div>
<!-- Tab Navigation -->
<div class="mt-6 border-b border-gray-200">
<nav class="-mb-px flex space-x-8">
<button
v-for="tab in ['overview', 'phases', 'tasks', 'analytics']"
:key="tab"
@click="activeTab = tab"
:class="[
'py-2 px-1 border-b-2 font-medium text-sm',
activeTab === tab
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
]"
>
{{ tab.charAt(0).toUpperCase() + tab.slice(1) }}
</button>
</nav>
</div>
</div>
</div>
<!-- Tab Content -->
<div class="px-6">
<!-- Overview Tab -->
<div v-if="activeTab === 'overview'" class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-2 space-y-6">
<!-- Phase Timeline -->
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-medium mb-4">Phase Timeline</h3>
<PhaseTimeline :phases="currentProject.phases" />
</div>
<!-- Recent Tasks -->
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-medium mb-4">Recent Tasks</h3>
<TaskList :tasks="currentProject.phases.flatMap(p => p.tasks).slice(0, 5)" />
</div>
</div>
<div class="space-y-6">
<!-- Project Stats -->
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-medium mb-4">Project Statistics</h3>
<div class="space-y-3">
<div class="flex justify-between">
<span class="text-gray-600">Total Phases</span>
<span class="font-medium">{{ currentProject.phases.length }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Completed Phases</span>
<span class="font-medium">{{ currentProject.phases.filter(p => p.status === 'completed').length }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Total Tasks</span>
<span class="font-medium">{{ currentProject.phases.flatMap(p => p.tasks).length }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Start Date</span>
<span class="font-medium">{{ new Date(currentProject.startDate).toLocaleDateString() }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Target Date</span>
<span class="font-medium">{{ new Date(currentProject.targetCompletionDate).toLocaleDateString() }}</span>
</div>
</div>
</div>
<!-- Deliverables -->
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-medium mb-4">Deliverables</h3>
<DeliverableTracker :project="currentProject" />
</div>
</div>
</div>
<!-- Other tabs would be implemented similarly -->
<div v-else-if="activeTab === 'phases'">
<PhaseManagement :project="currentProject" />
</div>
<div v-else-if="activeTab === 'tasks'">
<TaskBoard :project="currentProject" />
</div>
<div v-else-if="activeTab === 'analytics'">
<ProjectAnalytics :project="currentProject" />
</div>
</div>
</div>
<!-- Loading state -->
<div v-else class="flex justify-center items-center h-64">
<LoadingSpinner class="w-8 h-8" />
</div>
</template>
```
## State Management
### Nuxt Configuration
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
devtools: { enabled: true },
// CSS Framework
css: ['~/assets/css/main.css'],
// Modules
modules: [
'@nuxt/ui',
'@vueuse/nuxt',
'@pinia/nuxt'
],
// TypeScript
typescript: {
strict: true,
typeCheck: true
},
// Runtime Config
runtimeConfig: {
// Private keys (only available on the server-side)
taskMasterApiKey: process.env.TASKMASTER_API_KEY,
ragServiceApiKey: process.env.RAG_SERVICE_API_KEY,
mcpServerApiKey: process.env.MCP_SERVER_API_KEY,
// Public keys (exposed to client-side)
public: {
taskMasterUrl: process.env.TASKMASTER_URL || 'http://localhost:3001',
ragServiceUrl: process.env.RAG_SERVICE_URL || 'http://localhost:8000',
mcpServerUrl: process.env.MCP_SERVER_URL || 'http://localhost:3002'
}
},
// Route Rules for API caching
routeRules: {
// Cache API routes for 5 minutes
'/api/projects': { cache: { maxAge: 300 } },
'/api/infrastructure/services': { cache: { maxAge: 60 } },
// Disable caching for real-time endpoints
'/api/events/**': { cache: false },
'/api/rag/query': { cache: false }
},
// Server configuration
nitro: {
experimental: {
wasm: true
}
},
// Security headers
security: {
headers: {
contentSecurityPolicy: {
'img-src': ["'self'", 'data:', 'https:'],
'script-src': ["'self'", "'unsafe-inline'"],
'style-src': ["'self'", "'unsafe-inline'"]
}
}
}
})
```
### Type Definitions
```typescript
// types/domain.ts
export interface Project {
id: string
name: string
description: string
status: ProjectStatus
phases: Phase[]
startDate: Date
targetCompletionDate: Date
actualCompletionDate?: Date
resources: ResourceRequirement[]
risks: Risk[]
metadata: ProjectMetadata
}
export enum ProjectStatus {
PLANNED = 'planned',
IN_PROGRESS = 'in_progress',
PAUSED = 'paused',
COMPLETED = 'completed',
CANCELLED = 'cancelled'
}
export interface Phase {
id: string
name: string
description: string
status: PhaseStatus
tasks: Task[]
startDate: Date
endDate: Date
dependencies: string[]
deliverables: Deliverable[]
}
export interface Task {
id: string
title: string
description: string
status: TaskStatus
assignee?: string
priority: Priority
estimatedHours: number
actualHours?: number
startDate?: Date
endDate?: Date
dependencies: string[]
tags: string[]
}
export interface RAGSystem {
id: string
name: string
status: RAGSystemStatus
vectorStore: VectorStore
embeddingModel: EmbeddingModel
documents: DocumentIndex
queries: QueryHistory[]
performance: PerformanceMetrics
}
export interface RAGQuery {
id: string
query: string
timestamp: Date
resultCount: number
duration?: number
}
export interface RAGResult {
id: string
content: string
metadata: DocumentMetadata
score: number
source: string
}
// ... other domain types
```
```typescript
// types/events.ts
export interface DomainEvent {
id: string
type: string
aggregateId: string
aggregateType: string
version: number
timestamp: Date
data: Record<string, any>
metadata: EventMetadata
summary: string
}
export interface EventMetadata {
userId?: string
source: string
correlationId?: string
causationId?: string
}
// Event type constants
export const PROJECT_EVENTS = {
CREATED: 'project.created',
UPDATED: 'project.updated',
STATUS_CHANGED: 'project.status_changed',
PHASE_ADDED: 'project.phase_added',
TASK_ADDED: 'project.task_added'
} as const
export const INFRASTRUCTURE_EVENTS = {
SERVICE_STARTED: 'infrastructure.service_started',
SERVICE_STOPPED: 'infrastructure.service_stopped',
DEPLOYMENT_INITIATED: 'infrastructure.deployment_initiated',
DEPLOYMENT_COMPLETED: 'infrastructure.deployment_completed'
} as const
export const RAG_EVENTS = {
QUERY_EXECUTED: 'rag.query_executed',
DOCUMENT_INDEXED: 'rag.document_indexed',
SYSTEM_STATUS_CHANGED: 'rag.system_status_changed'
} as const
```
## Authentication & Security
### Authentication Integration
```typescript
// composables/useAuth.ts
export const useAuth = () => {
const user = ref<User | null>(null)
const isAuthenticated = computed(() => !!user.value)
const token = useCookie('auth-token', { secure: true, httpOnly: true })
const login = async (credentials: LoginCredentials): Promise<boolean> => {
try {
const { data } = await $fetch<{ user: User; token: string }>('/api/auth/login', {
method: 'POST',
body: credentials
})
user.value = data.user
token.value = data.token
await navigateTo('/dashboard')
return true
} catch (error) {
console.error('Login failed:', error)
return false
}
}
const logout = async (): Promise<void> => {
try {
await $fetch('/api/auth/logout', { method: 'POST' })
} catch (error) {
console.error('Logout error:', error)
} finally {
user.value = null
token.value = null
await navigateTo('/login')
}
}
const checkAuth = async (): Promise<boolean> => {
if (!token.value) return false
try {
const { data } = await $fetch<{ user: User }>('/api/auth/me')
user.value = data.user
return true
} catch (error) {
token.value = null
return false
}
}
return {
user: readonly(user),
isAuthenticated,
login,
logout,
checkAuth
}
}
```
### Auth Middleware
```typescript
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { isAuthenticated } = useAuth()
if (!isAuthenticated.value) {
return navigateTo('/login')
}
})
```
## Implementation Plan
### Phase 1: Foundation (Week 1-2)
1. **Project Setup**
- Initialize Nuxt 3 project with TypeScript
- Configure Tailwind CSS and Nuxt UI
- Set up ESLint, Prettier, and Husky
- Configure development environment
2. **Core Structure**
- Create directory structure
- Define TypeScript interfaces for domain model
- Set up basic composables architecture
- Implement basic routing
3. **API Integration**
- Create server API routes for projects
- Implement TaskMaster integration
- Set up error handling and validation
- Configure runtime config
### Phase 2: Core Features (Week 3-4)
1. **Project Management**
- Implement project listing and detail pages
- Create project creation and editing forms
- Build task management interface
- Add phase timeline visualization
2. **Dashboard**
- Create main dashboard layout
- Implement stats widgets
- Add activity feed
- Set up navigation
### Phase 3: Advanced Features (Week 5-6)
1. **RAG Integration**
- Build query interface
- Implement document explorer
- Add results visualization
- Create embedding viewer
2. **Real-time Features**
- Implement WebSocket/SSE connections
- Add event bus system
- Create notification system
- Build live status updates
### Phase 4: Infrastructure & MCP (Week 7-8)
1. **Infrastructure Management**
- Service monitoring dashboard
- Deployment status tracking
- Log viewer interface
- Metrics visualization
2. **MCP Integration**
- Server status monitoring
- Tool registry interface
- Client connection management
- Capability matrix visualization
### Phase 5: Polish & Testing (Week 9-10)
1. **UI/UX Refinement**
- Responsive design optimization
- Accessibility improvements
- Performance optimization
- Error boundary implementation
2. **Testing & Documentation**
- Unit testing with Vitest
- E2E testing with Playwright
- API testing
- Documentation writing
## Conclusion
This Vue/Nuxt web interface design provides a comprehensive solution for managing the RAG + MCP + TaskMaster ecosystem. The architecture leverages modern Vue 3 Composition API patterns, Nuxt 3's server-side capabilities, and event-driven real-time updates to create a robust and maintainable web application.
The design emphasizes:
- **Type Safety**: Full TypeScript integration with strong typing
- **Modularity**: Composable-based architecture for reusability
- **Performance**: Server-side rendering with smart caching
- **Real-time**: Event-driven updates for live data
- **Maintainability**: Clean separation of concerns and well-structured code
The implementation plan provides a clear path from basic setup to a fully-featured web interface, allowing for incremental development and testing.