fix(6229): remove the Profile settings page — nothing read it
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m9s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 43s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m9s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 43s
The Profile tab said an assistant used it to personalise chat and a daily journal. Neither exists, and nothing read the profile: the page saved to user_profiles and the backup carried it. Removed the tab, the client calls, /api/profile, the service and the model, and migration 0123 drops the table (downgrade recreates it empty). Backup format v27 stops writing user_profiles; an older archive carrying the section still restores, and the file-restore integration test now proves it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -500,24 +500,6 @@ export const createApiKey = (name: string, scope: 'read' | 'write') =>
|
||||
|
||||
export const revokeApiKey = (id: number) => apiDelete(`/api/api-keys/${id}`)
|
||||
|
||||
// ── User Profile ─────────────────────────────────────────────────────────────
|
||||
|
||||
export interface UserProfile {
|
||||
display_name: string
|
||||
job_title: string
|
||||
industry: string
|
||||
expertise_level: 'novice' | 'intermediate' | 'expert'
|
||||
response_style: 'concise' | 'balanced' | 'detailed'
|
||||
tone: 'casual' | 'professional' | 'technical'
|
||||
interests: string[]
|
||||
work_schedule: { days?: string[]; start?: string; end?: string }
|
||||
}
|
||||
|
||||
export const getProfile = () => apiGet<UserProfile>('/api/profile')
|
||||
export const updateProfile = (data: Partial<UserProfile>) =>
|
||||
apiPut<UserProfile>('/api/profile', data)
|
||||
|
||||
|
||||
// ── Note Versions (pinning) ──────────────────────────────────────────────────
|
||||
|
||||
import type { NoteVersion } from '../types/task'
|
||||
|
||||
@@ -5,10 +5,9 @@ import { useAuthStore } from "@/stores/auth";
|
||||
import { useToastStore } from "@/stores/toast";
|
||||
import { useCanonicalSystemsStore } from "@/stores/canonicalSystems";
|
||||
import { usePlatformsStore } from "@/stores/platforms";
|
||||
import { apiGet, apiPost, apiPut, apiDelete, listGroups, createGroup, deleteGroup, listGroupMembers, addGroupMember, removeGroupMember, searchUsers, listApiKeys, createApiKey as apiCreateApiKey, revokeApiKey as apiRevokeApiKey, getProfile, updateProfile, type ApiKeyEntry, type GroupEntry, type GroupMember, type UserSearchResult, type UserProfile, apiErrorMessage } from "@/api/client";
|
||||
import { apiGet, apiPost, apiPut, apiDelete, listGroups, createGroup, deleteGroup, listGroupMembers, addGroupMember, removeGroupMember, searchUsers, listApiKeys, createApiKey as apiCreateApiKey, revokeApiKey as apiRevokeApiKey, type ApiKeyEntry, type GroupEntry, type GroupMember, type UserSearchResult, apiErrorMessage } from "@/api/client";
|
||||
import type { User } from "@/types/auth";
|
||||
import PaginationBar from "@/components/PaginationBar.vue";
|
||||
import TagInput from "@/components/TagInput.vue";
|
||||
import MomentsSettings from "@/components/MomentsSettings.vue";
|
||||
import ReplyShapesSettings from "@/components/ReplyShapesSettings.vue";
|
||||
import BackupScheduleSettings from "@/components/BackupScheduleSettings.vue";
|
||||
@@ -530,7 +529,7 @@ async function copyCommit() {
|
||||
const restoreFileInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
// Migrate stored "admin" → "config"; unknown tabs fall back to "general"
|
||||
const VALID_TABS = new Set(["general", "account", "profile", "notifications", "integrations", "data", "apikeys", "config", "users", "logs", "groups", "areas", "platforms"]);
|
||||
const VALID_TABS = new Set(["general", "account", "notifications", "integrations", "data", "apikeys", "config", "users", "logs", "groups", "areas", "platforms"]);
|
||||
const _stored = localStorage.getItem("settings_tab") ?? "general";
|
||||
const activeTab = ref(VALID_TABS.has(_stored) ? (_stored === "admin" ? "config" : _stored) : "general");
|
||||
|
||||
@@ -846,50 +845,6 @@ const searchLoading = ref(false);
|
||||
const searchError = ref("");
|
||||
|
||||
|
||||
// ── Profile ──────────────────────────────────────────────────────────────────
|
||||
const WORK_DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||
const profile = ref<UserProfile>({
|
||||
display_name: '', job_title: '', industry: '',
|
||||
expertise_level: 'intermediate', response_style: 'balanced', tone: 'casual',
|
||||
interests: [], work_schedule: {},
|
||||
})
|
||||
const profileSaving = ref(false)
|
||||
const profileSaved = ref(false)
|
||||
|
||||
async function loadProfile() {
|
||||
try { profile.value = await getProfile() } catch { /* non-critical */ }
|
||||
}
|
||||
|
||||
async function saveProfile() {
|
||||
profileSaving.value = true
|
||||
profileSaved.value = false
|
||||
try {
|
||||
profile.value = await updateProfile({
|
||||
display_name: profile.value.display_name,
|
||||
job_title: profile.value.job_title,
|
||||
industry: profile.value.industry,
|
||||
expertise_level: profile.value.expertise_level,
|
||||
response_style: profile.value.response_style,
|
||||
tone: profile.value.tone,
|
||||
interests: profile.value.interests,
|
||||
work_schedule: profile.value.work_schedule,
|
||||
})
|
||||
profileSaved.value = true
|
||||
setTimeout(() => { profileSaved.value = false }, 2000)
|
||||
} catch { toastStore.show('Failed to save profile', 'error') }
|
||||
finally { profileSaving.value = false }
|
||||
}
|
||||
|
||||
function toggleProfileWorkDay(day: string) {
|
||||
const days = [...(profile.value.work_schedule.days ?? [])]
|
||||
const idx = days.indexOf(day)
|
||||
if (idx >= 0) days.splice(idx, 1)
|
||||
else days.push(day)
|
||||
profile.value.work_schedule = { ...profile.value.work_schedule, days }
|
||||
}
|
||||
|
||||
function emptyTagsFetch(): Promise<string[]> { return Promise.resolve([]) }
|
||||
|
||||
onMounted(async () => {
|
||||
await store.fetchSettings();
|
||||
newEmail.value = authStore.user?.email ?? "";
|
||||
@@ -974,9 +929,6 @@ onMounted(async () => {
|
||||
notifySecurityAlerts.value = allSettings.notify_security_alerts !== "false";
|
||||
}
|
||||
|
||||
// Load user profile
|
||||
await loadProfile();
|
||||
|
||||
// Check SearXNG status
|
||||
try {
|
||||
const sr = await apiGet<{ configured: boolean; searxng_url: string }>("/api/settings/search");
|
||||
@@ -1647,7 +1599,7 @@ async function deleteUser(userId: number) {
|
||||
<div class="sidebar-group">
|
||||
<div class="sidebar-group-label">User</div>
|
||||
<button
|
||||
v-for="tab in ['general', 'account', 'profile', 'notifications', 'integrations', 'data', 'apikeys']"
|
||||
v-for="tab in ['general', 'account', 'notifications', 'integrations', 'data', 'apikeys']"
|
||||
:key="tab"
|
||||
:class="['sidebar-item', { active: activeTab === tab }]"
|
||||
@click="activeTab = tab"
|
||||
@@ -2366,111 +2318,6 @@ async function deleteUser(userId: number) {
|
||||
</div>
|
||||
|
||||
<!-- ── Notifications ── -->
|
||||
<!-- ── Profile ── -->
|
||||
<div v-show="activeTab === 'profile'" class="settings-grid">
|
||||
<section class="settings-section full-width">
|
||||
<h2>About You</h2>
|
||||
<p class="section-desc">This information is used by the assistant to personalise responses in chat and the daily journal.</p>
|
||||
<div class="assistant-grid">
|
||||
<div class="field">
|
||||
<label>Display Name</label>
|
||||
<input v-model="profile.display_name" type="text" class="fs-input input" placeholder="e.g. Alex" />
|
||||
<p class="field-hint">How the assistant addresses you.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Job Title</label>
|
||||
<input v-model="profile.job_title" type="text" class="fs-input input" placeholder="e.g. Product Manager" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Industry</label>
|
||||
<input v-model="profile.industry" type="text" class="fs-input input" placeholder="e.g. Technology" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Expertise Level</label>
|
||||
<select v-model="profile.expertise_level" class="fs-input input">
|
||||
<option value="novice">Novice — explain things simply</option>
|
||||
<option value="intermediate">Intermediate — balanced explanations</option>
|
||||
<option value="expert">Expert — assume deep knowledge</option>
|
||||
</select>
|
||||
<p class="field-hint">Calibrates how the assistant explains concepts.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn-primary" @click="saveProfile" :disabled="profileSaving">{{ profileSaving ? 'Saving…' : 'Save' }}</button>
|
||||
<span v-if="profileSaved" class="saved-msg">Saved!</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section full-width">
|
||||
<h2>Response Preferences</h2>
|
||||
<div class="assistant-grid">
|
||||
<div class="field">
|
||||
<label>Response Style</label>
|
||||
<select v-model="profile.response_style" class="fs-input input">
|
||||
<option value="concise">Concise — short and direct</option>
|
||||
<option value="balanced">Balanced — default</option>
|
||||
<option value="detailed">Detailed — thorough explanations</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Tone</label>
|
||||
<select v-model="profile.tone" class="fs-input input">
|
||||
<option value="casual">Casual — friendly and relaxed</option>
|
||||
<option value="professional">Professional — formal and precise</option>
|
||||
<option value="technical">Technical — jargon-friendly</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn-primary" @click="saveProfile" :disabled="profileSaving">{{ profileSaving ? 'Saving…' : 'Save' }}</button>
|
||||
<span v-if="profileSaved" class="saved-msg">Saved!</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section full-width">
|
||||
<h2>Interests</h2>
|
||||
<p class="section-desc">Topics you care about — used to personalise the journal's daily prep and chat responses.</p>
|
||||
<TagInput v-model="profile.interests" placeholder="Add an interest…" :fetchTags="emptyTagsFetch" />
|
||||
<div class="actions">
|
||||
<button class="btn-primary" @click="saveProfile" :disabled="profileSaving">{{ profileSaving ? 'Saving…' : 'Save' }}</button>
|
||||
<span v-if="profileSaved" class="saved-msg">Saved!</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section full-width">
|
||||
<h2>Work Schedule</h2>
|
||||
<p class="section-desc">Helps the journal understand when you're working and what's relevant each morning.</p>
|
||||
<div class="field">
|
||||
<label>Work Days</label>
|
||||
<div class="day-picker">
|
||||
<button
|
||||
v-for="day in WORK_DAYS"
|
||||
:key="day"
|
||||
class="day-btn"
|
||||
:class="{ active: (profile.work_schedule.days ?? []).includes(day) }"
|
||||
@click="toggleProfileWorkDay(day)"
|
||||
type="button"
|
||||
>{{ day }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="assistant-grid" style="margin-top:0.75rem">
|
||||
<div class="field">
|
||||
<label>Start Time</label>
|
||||
<input v-model="profile.work_schedule.start" type="time" class="fs-input input" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>End Time</label>
|
||||
<input v-model="profile.work_schedule.end" type="time" class="fs-input input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn-primary" @click="saveProfile" :disabled="profileSaving">{{ profileSaving ? 'Saving…' : 'Save' }}</button>
|
||||
<span v-if="profileSaved" class="saved-msg">Saved!</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab === 'notifications'" class="settings-grid">
|
||||
|
||||
<section class="settings-section">
|
||||
@@ -3724,18 +3571,6 @@ async function deleteUser(userId: number) {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Assistant — 2-col internal grid */
|
||||
.assistant-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0 1rem;
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
.assistant-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@@ -3985,9 +3820,6 @@ async function deleteUser(userId: number) {
|
||||
.settings-section.full-width {
|
||||
grid-column: auto;
|
||||
}
|
||||
.assistant-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.smtp-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -4455,32 +4287,6 @@ async function deleteUser(userId: number) {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Profile tab ─────────────────────────────────────────────────────────── */
|
||||
.day-picker {
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
.day-btn {
|
||||
padding: 0.3rem 0.65rem;
|
||||
border: 1px solid var(--fs-border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--fs-surface-raised);
|
||||
color: var(--fs-text-tertiary);
|
||||
font-size: 0.82rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
font-family: inherit;
|
||||
}
|
||||
.day-btn:hover { border-color: var(--fs-accent); color: var(--fs-accent); }
|
||||
.day-btn.active {
|
||||
background: color-mix(in srgb, var(--fs-accent) 15%, transparent);
|
||||
border-color: var(--fs-accent);
|
||||
color: var(--fs-accent-fg);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ── Shared areas ──────────────────────────────────────────────────
|
||||
The slug is shown deliberately: it is what decides whether two names are
|
||||
the same area, and an admin renaming an entry needs to see it move. */
|
||||
|
||||
Reference in New Issue
Block a user