cc-dashboard/web/src/views/SettingsView.vue
Vadym Samoilenko 732e692c8a fix: implement 5-phase contract fixes, devops page, AI summaries, expanded assistant
Phase 1 — Contract bugs:
- Add progress_pct/budget_hours to ProjectHours schema and compute from ProjectBudget
- Add hours/pct to ToolUsage schema, compute in project_detail endpoint
- Fix ProjectDetailView to use correct nested data shape (data.project.*, data.sessions, etc.)
- Fix GenerateReportIn: rename field date → period_date; update reports router
- Fix tasks list date filter: use Query(alias='date') instead of positional arg
- Fix AzureIntegration/AzureWorkItem types: org→organization, id type, ado_id, sync_enabled
- Fix devops API payload and SettingsView to use organization field
- Fix TaskForm ADO work item selector to use wi.ado_id for display, wi.id for value
- Add light theme CSS variables in :root, keep dark in .dark class
- Remove hardcoded class='dark' from HTML, add theme persistence script
- TopBar: persist dark/light to localStorage on toggle
- DashboardView: switch monthly() → timeline() endpoint for charts
- DOW endpoint: add from/to date range filtering

Phase 2 — Planner:
- Add projects API endpoint and Pinia store
- Add project picker to TaskForm
- Fix ADO work item display (#ado_id not #id)

Phase 3 — Calendar:
- getWeekDays() accepts weekLength 5|7 parameter
- Calendar store: add weekLength ref, setWeekLength(), update fetchCurrentView range
- CalendarToolbar: add 5d/7d toggle buttons; fix dateLabel to use days[days.length-1]
- CalendarView: clicking session block navigates to project-detail/:id/:date
- project-detail route: add optional :date? param; ProjectDetailView filters by date
- DnD resize: send start_at alongside end_at (PlannedBlockIn requires both fields)

Phase 4 — AI session summaries:
- Add ai_title/ai_result columns to Session model
- Alembic migration 0006 for new columns
- New ai_session_summary service using Claude Haiku
- Session summarize endpoint: POST /api/dashboard/sessions/{id}/summarize
- Scheduler job: summarize sessions without ai_title every 10 minutes
- SessionOut schema: add ai_title/ai_result fields
- ProjectDetailView: show ai_title as primary, ai_result as subtitle; sparkle button to generate

Phase 5 — Expanded AI assistant:
- Add 14 new tools: list/create/update/delete/complete tasks, prioritize_day,
  schedule_task, auto_schedule_day, list_projects, list/delete manual entries,
  generate_report, search_sessions, list_work_items
- Import PlannedBlock and AzureWorkItem in assistant service
- Update SYSTEM_PROMPT to describe full agent capabilities
- Agentic loop: 5 → 10 rounds max
- AssistantWidget: add tool labels for all new tools, update quick hints

New files: DevopsView.vue, projects store/API, ai_session_summary.py, migration 0006

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 21:13:28 +01:00

226 lines
7 KiB
Vue

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { useDevopsStore } from '@/stores/devops'
import Card from '@/components/ui/Card.vue'
import CardHeader from '@/components/ui/CardHeader.vue'
import CardTitle from '@/components/ui/CardTitle.vue'
import CardContent from '@/components/ui/CardContent.vue'
import Input from '@/components/ui/Input.vue'
import Button from '@/components/ui/Button.vue'
import { toast } from 'vue-sonner'
import { downloadCsv, downloadIcs } from '@/api/endpoints/exports'
import { isoDateStr } from '@/lib/utils'
import apiClient from '@/api/client'
const authStore = useAuthStore()
const devopsStore = useDevopsStore()
// Profile
const username = ref('')
const dailyOverhead = ref(0)
const savingProfile = ref(false)
// ADO
const adoOrg = ref('')
const adoProject = ref('')
const adoPat = ref('')
const savingAdo = ref(false)
// Export
const exportFrom = ref('')
const exportTo = ref('')
onMounted(() => {
if (authStore.user) {
username.value = authStore.user.username
dailyOverhead.value = authStore.user.daily_overhead_hours ?? 0
}
devopsStore.fetchIntegration().then(() => {
if (devopsStore.integration) {
adoOrg.value = devopsStore.integration.organization
adoProject.value = devopsStore.integration.project
}
})
const now = new Date()
exportTo.value = isoDateStr(now)
const past = new Date(now)
past.setDate(now.getDate() - 30)
exportFrom.value = isoDateStr(past)
})
async function saveProfile() {
savingProfile.value = true
try {
await apiClient.patch('/api/auth/me', {
username: username.value,
daily_overhead_hours: dailyOverhead.value,
})
await authStore.fetchMe()
toast.success('Profile saved')
} catch {
toast.error('Failed to save profile')
} finally {
savingProfile.value = false
}
}
async function saveAdo() {
if (!adoOrg.value || !adoProject.value || !adoPat.value) {
toast.error('All ADO fields are required')
return
}
savingAdo.value = true
try {
await devopsStore.saveIntegration({
organization: adoOrg.value,
project: adoProject.value,
pat: adoPat.value,
})
adoPat.value = ''
toast.success('Integration saved')
} catch {
toast.error('Failed to save integration')
} finally {
savingAdo.value = false
}
}
async function deleteAdo() {
if (!confirm('Delete ADO integration?')) return
try {
await devopsStore.deleteIntegration()
adoOrg.value = ''
adoProject.value = ''
adoPat.value = ''
toast.success('Integration deleted')
} catch {
toast.error('Failed to delete integration')
}
}
async function syncNow() {
try {
await devopsStore.sync()
toast.success('Sync complete')
} catch {
toast.error(devopsStore.error ?? 'Sync failed')
}
}
</script>
<template>
<div class="p-6 space-y-6 max-w-2xl">
<h2 class="text-lg font-semibold text-foreground">Settings</h2>
<!-- Profile -->
<Card>
<CardHeader>
<CardTitle class="text-sm">Profile</CardTitle>
</CardHeader>
<CardContent class="space-y-4">
<div class="space-y-1.5">
<label class="text-sm font-medium text-foreground">Username</label>
<Input v-model="username" placeholder="username" />
</div>
<div class="space-y-1.5">
<label class="text-sm font-medium text-foreground">Daily Overhead Hours</label>
<Input v-model="dailyOverhead" type="number" min="0" max="8" step="0.25" class="w-32" />
<p class="text-xs text-muted-foreground">
Hours per day to add for overhead / meetings
</p>
</div>
<Button :loading="savingProfile" @click="saveProfile">Save Profile</Button>
</CardContent>
</Card>
<!-- Azure DevOps Integration -->
<Card>
<CardHeader>
<CardTitle class="text-sm">Azure DevOps Integration</CardTitle>
</CardHeader>
<CardContent class="space-y-4">
<div v-if="devopsStore.integration" class="text-xs text-muted-foreground space-y-1">
<p>
Connected to <strong class="text-foreground">{{ devopsStore.integration.organization }}</strong>
/ <strong class="text-foreground">{{ devopsStore.integration.project }}</strong>
</p>
<p v-if="devopsStore.integration.last_synced_at">
Last synced: {{ new Date(devopsStore.integration.last_synced_at).toLocaleString() }}
</p>
<p v-if="devopsStore.integration.last_sync_error" class="text-red-400">
Error: {{ devopsStore.integration.last_sync_error }}
</p>
</div>
<div class="grid grid-cols-2 gap-3">
<div class="space-y-1.5">
<label class="text-sm font-medium text-foreground">Organization</label>
<Input v-model="adoOrg" placeholder="myorg" />
</div>
<div class="space-y-1.5">
<label class="text-sm font-medium text-foreground">Project</label>
<Input v-model="adoProject" placeholder="myproject" />
</div>
</div>
<div class="space-y-1.5">
<label class="text-sm font-medium text-foreground">
Personal Access Token {{ devopsStore.integration ? '(leave blank to keep current)' : '' }}
</label>
<Input v-model="adoPat" type="password" placeholder="••••••••" autocomplete="new-password" />
</div>
<div class="flex items-center gap-2">
<Button :loading="savingAdo" @click="saveAdo">
{{ devopsStore.integration ? 'Update' : 'Connect' }}
</Button>
<Button
v-if="devopsStore.integration"
variant="outline"
:loading="devopsStore.syncing"
@click="syncNow"
>
Sync Now
</Button>
<Button
v-if="devopsStore.integration"
variant="destructive"
size="sm"
@click="deleteAdo"
>
Disconnect
</Button>
</div>
</CardContent>
</Card>
<!-- Export -->
<Card>
<CardHeader>
<CardTitle class="text-sm">Export</CardTitle>
</CardHeader>
<CardContent class="space-y-4">
<div class="flex items-center gap-3 flex-wrap">
<div class="space-y-1.5">
<label class="text-xs text-muted-foreground">From</label>
<Input v-model="exportFrom" type="date" class="h-8 text-xs" />
</div>
<div class="space-y-1.5">
<label class="text-xs text-muted-foreground">To</label>
<Input v-model="exportTo" type="date" class="h-8 text-xs" />
</div>
</div>
<div class="flex items-center gap-2">
<Button variant="outline" size="sm" @click="downloadCsv(exportFrom, exportTo)">
Download CSV
</Button>
<Button variant="outline" size="sm" @click="downloadIcs(exportFrom, exportTo)">
Download ICS
</Button>
</div>
</CardContent>
</Card>
</div>
</template>