refactor: Phase 8 — backend deletion (chat / voice / push / journal / curator)
Mega-commit. Strips all server-side LLM machinery now that Phase 7 has
removed the corresponding UI surfaces and the MCP HTTP endpoint is the
sole assistant interface.
Deleted (services/):
chat, generation_buffer, generation_log, generation_task, llm, tools/
(entire package), stt, tts, voice_config, voice_library, push,
journal_closeout, journal_pipeline, journal_prep, journal_scheduler,
journal_search, curator, curator_scheduler, consolidation,
tag_suggestions, research, weather, article_fetcher, pending_actions,
moments, assist, wikipedia.
Deleted (routes/):
chat, voice, push, journal, quick_capture, fable_mcp_dist.
Deleted (models/):
conversation, generation_tool_log, push_subscription,
pending_curator_action, moment, weather_cache.
Deleted (tests/):
test_generation_log, test_journal_*, test_consolidation, test_lookup_tool,
test_notes_consolidation_trigger, test_record_moment_guards,
test_research_pipeline, test_tools_*, test_tool_use_fixes,
test_voice_library, test_weather_service, test_calendar_tool_tz,
test_wikipedia.
Deleted (top-level):
fable-mcp/ (legacy standalone stdio package — wheel-build pipeline
also removed from Dockerfile).
app.py:
- blueprint registrations for the 6 deleted routes
- startup hook trimmed: no more Ollama warmup, KV-cache priming,
journal/curator schedulers, voice model loading
- shutdown hook simplified
- httpx import dropped (was for Ollama calls)
pyproject.toml:
- removed deps: pywebpush, feedparser, html2text, trafilatura
- removed [voice] extras entirely
- description updated for the MCP-first architecture
Dockerfile:
- removed faster-whisper / piper-tts install steps
- removed bundled piper voice download stage
- removed fable-mcp wheel build stage
Surviving-file edits:
- services/auth.py: drop Conversation table claim on first-user setup
- services/backup.py: drop conversation / push-subscription export+restore;
v1/v2 restore now silently skip pre-pivot conversation data
- services/notes.py: drop maybe_consolidate trigger on task done/cancelled;
drop _maybe_trigger_project_summary (LLM auto-summary)
- services/projects.py: drop generate_project_summary + backfill_project_summaries
(both LLM-driven)
- services/user_profile.py: drop append_observations / consolidate /
clear_learned_data (curator-tied) and build_profile_context
(was LLM system-prompt builder)
- services/notifications.py: stub out _fire_push_notif (was send_push_notification)
- services/event_scheduler.py: drop event-reminder push + chat-retention
cleanup job; keep CalDAV pull-sync + reminders job (in-app)
- services/diagnostics.py: _curator_busy() always False
- routes/notes.py: drop /assist, /assist/stream, /suggest-tags endpoints
- routes/tasks.py: drop /<id>/consolidate endpoint
- routes/settings.py: drop /models, KV-cache-prime-on-save, journal-schedule
timezone hook, and the SearXNG search-test endpoint; inline _is_private_url
(was in services/llm.py)
- routes/admin.py: drop /voice, /voice/reload endpoints
- routes/profile.py: drop /consolidate, /observations (GET, DELETE)
- models/__init__.py: drop the 6 dead model imports
Frontend cascade:
- stores/push.ts: deleted entirely (no callers after Phase 7)
- stores/settings.ts: drop checkVoiceStatus + voice-status state
- views/SettingsView.vue: drop Locations section + journalConfig state
(was tied to /api/journal/config); drop JournalConfig + journal/voice
api/client imports
- frontend/api/client.ts: orphaned voice/journal/profile-observation/
fable-mcp-dist exports are left as dead but harmless (call them and
they 404; type-check is clean).
Pre-existing v1 backups that contained conversations/messages still
restore — those tables are silently dropped from the import path.
Anyone pulling the new image with a populated database will need the
Phase 9 migration to drop the dead tables (coming next).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,109 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
function urlBase64ToUint8Array(base64String: string): Uint8Array<ArrayBuffer> {
|
||||
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
|
||||
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
|
||||
const rawData = window.atob(base64)
|
||||
const buf = new ArrayBuffer(rawData.length)
|
||||
const outputArray = new Uint8Array<ArrayBuffer>(buf)
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i)
|
||||
}
|
||||
return outputArray
|
||||
}
|
||||
|
||||
export const usePushStore = defineStore('push', () => {
|
||||
const isSupported = ref(
|
||||
'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window
|
||||
)
|
||||
const permission = ref<NotificationPermission>(
|
||||
'Notification' in window ? Notification.permission : 'denied'
|
||||
)
|
||||
const isSubscribed = ref(false)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
async function checkSubscription(): Promise<void> {
|
||||
if (!isSupported.value) return
|
||||
try {
|
||||
const reg = await navigator.serviceWorker.ready
|
||||
const sub = await reg.pushManager.getSubscription()
|
||||
isSubscribed.value = !!sub
|
||||
} catch {
|
||||
isSubscribed.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function subscribe(): Promise<void> {
|
||||
if (!isSupported.value) {
|
||||
error.value = 'Push notifications not supported in this browser'
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
// Request permission
|
||||
permission.value = await Notification.requestPermission()
|
||||
if (permission.value !== 'granted') {
|
||||
error.value = 'Notification permission denied'
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch VAPID public key
|
||||
const resp = await fetch('/api/push/vapid-public-key')
|
||||
if (!resp.ok) {
|
||||
error.value = 'Push notifications not configured on server'
|
||||
return
|
||||
}
|
||||
const { publicKey } = await resp.json()
|
||||
|
||||
// Register service worker
|
||||
const reg = await navigator.serviceWorker.register('/sw.js')
|
||||
await navigator.serviceWorker.ready
|
||||
|
||||
// Subscribe
|
||||
const sub = await reg.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(publicKey),
|
||||
})
|
||||
|
||||
// Save to server
|
||||
const saveResp = await fetch('/api/push/subscribe', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(sub.toJSON()),
|
||||
})
|
||||
if (!saveResp.ok) throw new Error('Failed to save subscription')
|
||||
isSubscribed.value = true
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to subscribe'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function unsubscribe(): Promise<void> {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const reg = await navigator.serviceWorker.ready
|
||||
const sub = await reg.pushManager.getSubscription()
|
||||
if (sub) {
|
||||
await fetch('/api/push/subscribe', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ endpoint: sub.endpoint }),
|
||||
})
|
||||
await sub.unsubscribe()
|
||||
}
|
||||
isSubscribed.value = false
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to unsubscribe'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return { isSupported, permission, isSubscribed, loading, error, checkSubscription, subscribe, unsubscribe }
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ref, computed } from "vue";
|
||||
import { ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { apiGet, apiPut, getVoiceStatus } from "@/api/client";
|
||||
import { apiGet, apiPut } from "@/api/client";
|
||||
import { useToastStore } from "@/stores/toast";
|
||||
import type { AppSettings } from "@/types/settings";
|
||||
|
||||
@@ -8,32 +8,6 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
const settings = ref<AppSettings>({});
|
||||
const loading = ref(false);
|
||||
|
||||
const assistantName = computed(
|
||||
() => settings.value.assistant_name || "Fable"
|
||||
);
|
||||
|
||||
const defaultModel = computed(
|
||||
() => settings.value.default_model || ""
|
||||
);
|
||||
|
||||
// Voice status — checked once on login, refreshable from Settings
|
||||
const voiceEnabled = ref(false);
|
||||
const voiceSttReady = ref(false);
|
||||
const voiceTtsReady = ref(false);
|
||||
|
||||
async function checkVoiceStatus() {
|
||||
try {
|
||||
const s = await getVoiceStatus();
|
||||
voiceEnabled.value = s.enabled;
|
||||
voiceSttReady.value = s.enabled && s.stt;
|
||||
voiceTtsReady.value = s.enabled && s.tts;
|
||||
} catch {
|
||||
voiceEnabled.value = false;
|
||||
voiceSttReady.value = false;
|
||||
voiceTtsReady.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSettings() {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -60,12 +34,6 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
return {
|
||||
settings,
|
||||
loading,
|
||||
assistantName,
|
||||
defaultModel,
|
||||
voiceEnabled,
|
||||
voiceSttReady,
|
||||
voiceTtsReady,
|
||||
checkVoiceStatus,
|
||||
fetchSettings,
|
||||
updateSettings,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ref, computed, watch, onMounted } from "vue";
|
||||
import { useSettingsStore } from "@/stores/settings";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useToastStore } from "@/stores/toast";
|
||||
import { apiGet, apiPost, apiPut, apiDelete, listGroups, createGroup, deleteGroup, listGroupMembers, addGroupMember, removeGroupMember, searchUsers, listApiKeys, createApiKey as apiCreateApiKey, revokeApiKey as apiRevokeApiKey, getProfile, updateProfile, getJournalConfig, saveJournalConfig, geocodeAddress, type ApiKeyEntry, type GroupEntry, type GroupMember, type UserSearchResult, type UserProfile, type JournalConfig } from "@/api/client";
|
||||
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 } from "@/api/client";
|
||||
import type { User } from "@/types/auth";
|
||||
import PaginationBar from "@/components/PaginationBar.vue";
|
||||
import TagInput from "@/components/TagInput.vue";
|
||||
@@ -373,95 +373,6 @@ function toggleProfileWorkDay(day: string) {
|
||||
|
||||
function emptyTagsFetch(): Promise<string[]> { return Promise.resolve([]) }
|
||||
|
||||
// ── Journal config (locations, temp unit, prep schedule) ────────────────────
|
||||
const journalConfig = ref<JournalConfig>({
|
||||
prep_enabled: true,
|
||||
prep_hour: 5,
|
||||
prep_minute: 0,
|
||||
day_rollover_hour: 4,
|
||||
closeout_enabled: true,
|
||||
locations: { home: { label: 'Home', address: '' }, work: { label: 'Work', address: '' } },
|
||||
temp_unit: 'C',
|
||||
})
|
||||
const journalConfigSaving = ref(false)
|
||||
const journalConfigSaved = ref(false)
|
||||
const homeQuery = ref('')
|
||||
const workQuery = ref('')
|
||||
const homeGeoStatus = ref<'' | 'ok' | 'error' | 'pending'>('')
|
||||
const workGeoStatus = ref<'' | 'ok' | 'error' | 'pending'>('')
|
||||
const homeGeoMsg = ref('')
|
||||
const workGeoMsg = ref('')
|
||||
|
||||
async function loadJournalConfig() {
|
||||
try {
|
||||
const cfg = await getJournalConfig()
|
||||
journalConfig.value = {
|
||||
prep_enabled: cfg.prep_enabled ?? true,
|
||||
prep_hour: cfg.prep_hour ?? 5,
|
||||
prep_minute: cfg.prep_minute ?? 0,
|
||||
day_rollover_hour: cfg.day_rollover_hour ?? 4,
|
||||
closeout_enabled: cfg.closeout_enabled ?? true,
|
||||
morning_end_hour: cfg.morning_end_hour,
|
||||
midday_end_hour: cfg.midday_end_hour,
|
||||
locations: {
|
||||
home: cfg.locations?.home ?? { label: 'Home', address: '' },
|
||||
work: cfg.locations?.work ?? { label: 'Work', address: '' },
|
||||
},
|
||||
temp_unit: cfg.temp_unit ?? 'C',
|
||||
}
|
||||
homeQuery.value = cfg.locations?.home?.address ?? ''
|
||||
workQuery.value = cfg.locations?.work?.address ?? ''
|
||||
} catch { /* non-critical */ }
|
||||
}
|
||||
|
||||
async function geocodeFor(which: 'home' | 'work') {
|
||||
const query = (which === 'home' ? homeQuery.value : workQuery.value).trim()
|
||||
const statusRef = which === 'home' ? homeGeoStatus : workGeoStatus
|
||||
const msgRef = which === 'home' ? homeGeoMsg : workGeoMsg
|
||||
const label = which === 'home' ? 'Home' : 'Work'
|
||||
if (!query) {
|
||||
// Clear location
|
||||
if (!journalConfig.value.locations) journalConfig.value.locations = {}
|
||||
journalConfig.value.locations[which] = { label, address: '' }
|
||||
statusRef.value = ''
|
||||
msgRef.value = ''
|
||||
return
|
||||
}
|
||||
statusRef.value = 'pending'
|
||||
msgRef.value = 'Looking up…'
|
||||
try {
|
||||
const result = await geocodeAddress(query)
|
||||
if (!result) {
|
||||
statusRef.value = 'error'
|
||||
msgRef.value = `No match for "${query}"`
|
||||
return
|
||||
}
|
||||
if (!journalConfig.value.locations) journalConfig.value.locations = {}
|
||||
journalConfig.value.locations[which] = {
|
||||
label,
|
||||
address: query,
|
||||
lat: result.lat,
|
||||
lon: result.lon,
|
||||
}
|
||||
statusRef.value = 'ok'
|
||||
msgRef.value = `Found: ${result.display_name}`
|
||||
} catch {
|
||||
statusRef.value = 'error'
|
||||
msgRef.value = 'Geocoding failed'
|
||||
}
|
||||
}
|
||||
|
||||
async function saveJournalCfg() {
|
||||
journalConfigSaving.value = true
|
||||
journalConfigSaved.value = false
|
||||
try {
|
||||
await saveJournalConfig(journalConfig.value)
|
||||
journalConfigSaved.value = true
|
||||
setTimeout(() => { journalConfigSaved.value = false }, 2000)
|
||||
} catch { toastStore.show('Failed to save journal settings', 'error') }
|
||||
finally { journalConfigSaving.value = false }
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const v = await apiGet<{ version: string }>('/api/version')
|
||||
@@ -486,7 +397,6 @@ onMounted(async () => {
|
||||
await loadProfile();
|
||||
|
||||
// Load journal config (locations, temp unit; prep/closeout UI removed in Phase 7)
|
||||
await loadJournalConfig();
|
||||
|
||||
// Load CalDAV settings
|
||||
try {
|
||||
@@ -1330,64 +1240,6 @@ function formatUserDate(iso: string): string {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section full-width">
|
||||
<h2>Locations</h2>
|
||||
<p class="section-desc">Home and work locations are used by the journal's daily prep to fetch local weather. Place names are geocoded once on save.</p>
|
||||
<div class="field">
|
||||
<label>Home</label>
|
||||
<div class="location-row">
|
||||
<input
|
||||
v-model="homeQuery"
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="e.g. Brooklyn, NY"
|
||||
@blur="geocodeFor('home')"
|
||||
@keydown.enter.prevent="geocodeFor('home')"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="homeGeoStatus === 'ok'" class="geo-msg geo-ok">{{ homeGeoMsg }}</p>
|
||||
<p v-else-if="homeGeoStatus === 'error'" class="geo-msg geo-error">{{ homeGeoMsg }}</p>
|
||||
<p v-else-if="homeGeoStatus === 'pending'" class="geo-msg geo-pending">{{ homeGeoMsg }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Work</label>
|
||||
<div class="location-row">
|
||||
<input
|
||||
v-model="workQuery"
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="e.g. Manhattan, NY"
|
||||
@blur="geocodeFor('work')"
|
||||
@keydown.enter.prevent="geocodeFor('work')"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="workGeoStatus === 'ok'" class="geo-msg geo-ok">{{ workGeoMsg }}</p>
|
||||
<p v-else-if="workGeoStatus === 'error'" class="geo-msg geo-error">{{ workGeoMsg }}</p>
|
||||
<p v-else-if="workGeoStatus === 'pending'" class="geo-msg geo-pending">{{ workGeoMsg }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Temperature unit</label>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
class="unit-btn"
|
||||
:class="{ active: journalConfig.temp_unit === 'C' }"
|
||||
@click="journalConfig.temp_unit = 'C'"
|
||||
>Celsius</button>
|
||||
<button
|
||||
type="button"
|
||||
class="unit-btn"
|
||||
:class="{ active: journalConfig.temp_unit === 'F' }"
|
||||
@click="journalConfig.temp_unit = 'F'"
|
||||
>Fahrenheit</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn-save" @click="saveJournalCfg" :disabled="journalConfigSaving">{{ journalConfigSaving ? 'Saving…' : 'Save' }}</button>
|
||||
<span v-if="journalConfigSaved" class="saved-msg">Saved!</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab === 'notifications'" class="settings-grid">
|
||||
|
||||
Reference in New Issue
Block a user