fix(settings): address profile tab code quality issues

- add loadError ref to useProfileStore, rendered in MyProfileView
- replace raw fetch with useApiFetch in generateSummary/generateMissions
- remove await from sync-identity call (fire-and-forget)
- add stable id field to MissionPref, use as v-for key
- add test for load() error path
This commit is contained in:
pyr0ball 2026-03-21 02:37:53 -07:00
parent a8b16d616c
commit 86454a97be
3 changed files with 50 additions and 25 deletions

View file

@ -41,4 +41,11 @@ describe('useProfileStore', () => {
await store.save()
expect(store.saveError).toBeTruthy()
})
it('sets loadError when load fails', async () => {
mockFetch.mockResolvedValueOnce({ data: null, error: { kind: 'network', message: 'Network error' } })
const store = useProfileStore()
await store.load()
expect(store.loadError).toBe('Network error')
})
})

View file

@ -2,7 +2,7 @@ import { ref } from 'vue'
import { defineStore } from 'pinia'
import { useApiFetch } from '../../composables/useApi'
export interface MissionPref { industry: string; note: string }
export interface MissionPref { id: string; industry: string; note: string }
export const useProfileStore = defineStore('settings/profile', () => {
const name = ref('')
@ -20,11 +20,17 @@ export const useProfileStore = defineStore('settings/profile', () => {
const loading = ref(false)
const saving = ref(false)
const saveError = ref<string | null>(null)
const loadError = ref<string | null>(null)
async function load() {
loading.value = true
const { data } = await useApiFetch<Record<string, unknown>>('/api/settings/profile')
loadError.value = null
const { data, error } = await useApiFetch<Record<string, unknown>>('/api/settings/profile')
loading.value = false
if (error) {
loadError.value = error.kind === 'network' ? error.message : error.detail || 'Failed to load profile'
return
}
if (!data) return
name.value = String(data.name ?? '')
email.value = String(data.email ?? '')
@ -33,7 +39,8 @@ export const useProfileStore = defineStore('settings/profile', () => {
career_summary.value = String(data.career_summary ?? '')
candidate_voice.value = String(data.candidate_voice ?? '')
inference_profile.value = String(data.inference_profile ?? 'cpu')
mission_preferences.value = (data.mission_preferences as MissionPref[]) ?? []
mission_preferences.value = ((data.mission_preferences as Array<{ industry: string; note: string }>) ?? [])
.map((m) => ({ id: crypto.randomUUID(), industry: m.industry ?? '', note: m.note ?? '' }))
nda_companies.value = (data.nda_companies as string[]) ?? []
accessibility_focus.value = Boolean(data.accessibility_focus)
lgbtq_focus.value = Boolean(data.lgbtq_focus)
@ -65,8 +72,8 @@ export const useProfileStore = defineStore('settings/profile', () => {
saveError.value = 'Save failed — please try again.'
return
}
// Push identity fields to resume YAML — graceful; endpoint may not exist yet (Task 3)
await useApiFetch('/api/settings/resume/sync-identity', {
// fire-and-forget — identity sync failures don't block save
useApiFetch('/api/settings/resume/sync-identity', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@ -81,7 +88,7 @@ export const useProfileStore = defineStore('settings/profile', () => {
return {
name, email, phone, linkedin_url, career_summary, candidate_voice, inference_profile,
mission_preferences, nda_companies, accessibility_focus, lgbtq_focus,
loading, saving, saveError,
loading, saving, saveError, loadError,
load, save,
}
})

View file

@ -8,6 +8,9 @@
<div v-if="store.loading" class="loading-state">Loading profile</div>
<template v-else>
<div v-if="loadError" class="load-error-banner" role="alert">
<strong>Error loading profile:</strong> {{ loadError }}
</div>
<!-- Identity -->
<section class="form-section">
<h3 class="section-title">Identity</h3>
@ -88,7 +91,7 @@
<div
v-for="(pref, idx) in store.mission_preferences"
:key="idx"
:key="pref.id"
class="mission-row"
>
<input
@ -197,8 +200,10 @@
import { ref, onMounted } from 'vue'
import { useProfileStore } from '../../stores/settings/profile'
import { useAppConfigStore } from '../../stores/appConfig'
import { useApiFetch } from '../../composables/useApi'
const store = useProfileStore()
const { loadError } = store
const config = useAppConfigStore()
const newNdaCompany = ref('')
@ -209,7 +214,7 @@ onMounted(() => { store.load() })
// Mission helpers
function addMission() {
store.mission_preferences = [...store.mission_preferences, { industry: '', note: '' }]
store.mission_preferences = [...store.mission_preferences, { id: crypto.randomUUID(), industry: '', note: '' }]
}
function removeMission(idx: number) {
@ -240,27 +245,23 @@ function autosave() {
// AI generation (paid tier)
async function generateSummary() {
generatingSummary.value = true
try {
const res = await fetch('/api/settings/profile/generate-summary', { method: 'POST' })
if (res.ok) {
const data = await res.json() as { summary?: string }
if (data.summary) store.career_summary = data.summary
}
} finally {
generatingSummary.value = false
}
const { data, error } = await useApiFetch<{ summary?: string }>(
'/api/settings/profile/generate-summary', { method: 'POST' }
)
generatingSummary.value = false
if (!error && data?.summary) store.career_summary = data.summary
}
async function generateMissions() {
generatingMissions.value = true
try {
const res = await fetch('/api/settings/profile/generate-missions', { method: 'POST' })
if (res.ok) {
const data = await res.json() as { mission_preferences?: Array<{ industry: string; note: string }> }
if (data.mission_preferences) store.mission_preferences = data.mission_preferences
}
} finally {
generatingMissions.value = false
const { data, error } = await useApiFetch<{ mission_preferences?: Array<{ industry: string; note: string }> }>(
'/api/settings/profile/generate-missions', { method: 'POST' }
)
generatingMissions.value = false
if (!error && data?.mission_preferences) {
store.mission_preferences = data.mission_preferences.map((m) => ({
id: crypto.randomUUID(), industry: m.industry ?? '', note: m.note ?? '',
}))
}
}
</script>
@ -292,6 +293,16 @@ async function generateMissions() {
padding: var(--space-4) 0;
}
.load-error-banner {
padding: var(--space-3) var(--space-4);
margin-bottom: var(--space-4);
background: color-mix(in srgb, var(--color-danger, #c0392b) 10%, transparent);
border: 1px solid color-mix(in srgb, var(--color-danger, #c0392b) 40%, transparent);
border-radius: 6px;
color: var(--color-danger, #c0392b);
font-size: 0.875rem;
}
/* ── Sections ──────────────────────────────────────────── */
.form-section {
border: 1px solid var(--color-border);