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>
226 lines
7 KiB
Vue
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>
|