Add Projects, Milestones, RAG auto-inject, push notifications, PWA, tag normalisation

## Projects & Milestones (Phases A + G)
- New models: Project, Milestone (Project → Milestone → Task hierarchy)
- notes table: project_id + milestone_id FKs; parent_id FK constraint activated
- Migrations: 0017 (projects), 0018 (push_subscriptions), 0019 (events), 0020 (milestones)
- Services: projects.py, milestones.py (CRUD + progress tracking)
- Routes: /api/projects + /api/projects/<id>/milestones
- LLM tools: create/list/get/update project; create/list milestone; project + milestone + parent_task params on note/task tools
- Frontend: ProjectListView (stacked milestone bars), ProjectView (milestone-grouped kanban), ProjectSelector, MilestoneSelector, NoteEditorView + TaskEditorView updated

## RAG Auto-injection (Phase B)
- Notes ≥0.60 cosine similarity auto-injected into system prompt (max 3, 800 chars each)
- excluded_note_ids param; ChatView "Auto-included" sidebar section

## Summarisation improvements (Phase C)
- Threshold 20→30, keep-recent 6→8, max_tokens 200→400
- Two-pass summarisation for histories >50 messages

## Browser push notifications (Phase E)
- PushSubscription model + migration; pywebpush dependency
- /api/push routes; VAPID config; fire-and-forget on generation complete
- Frontend: sw.js, push store, Settings toggle

## PWA manifest (Phase F)
- manifest.json, Apple meta tags, service worker registration in main.ts

## Tag normalisation
- All tags lowercased + deduplicated at backend (create_note/update_note) and frontend (TagInput sanitize)
- Note/Task types gain project_id + milestone_id fields; store signatures updated

## CalDAV
- Radicale embedded server reverted; back to user-configured external CalDAV

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 20:52:21 -05:00
parent 3d7be5888e
commit 012eb1d46b
52 changed files with 4319 additions and 62 deletions
+93
View File
@@ -4,10 +4,12 @@ import { useSettingsStore } from "@/stores/settings";
import { useAuthStore } from "@/stores/auth";
import { useToastStore } from "@/stores/toast";
import { apiGet, apiPost, apiPut } from "@/api/client";
import { usePushStore } from "@/stores/push";
const store = useSettingsStore();
const authStore = useAuthStore();
const toastStore = useToastStore();
const pushStore = usePushStore();
const assistantName = ref("");
const defaultModel = ref("");
const installedModels = ref<string[]>([]);
@@ -73,6 +75,7 @@ const searchError = ref("");
onMounted(async () => {
await store.fetchSettings();
pushStore.checkSubscription();
assistantName.value = store.assistantName;
newEmail.value = authStore.user?.email ?? "";
@@ -640,6 +643,59 @@ function hostname(url: string): string {
</template>
</section>
<!-- Push Notifications half width -->
<section class="settings-section">
<h2>Push Notifications</h2>
<p class="section-desc">
Receive browser push notifications when a response is ready.
</p>
<template v-if="!pushStore.isSupported">
<p class="push-unsupported">Push notifications are not supported in this browser.</p>
</template>
<template v-else>
<div class="push-status-row">
<span class="push-status-label">Permission:</span>
<span
:class="['push-permission-badge', {
'perm-granted': pushStore.permission === 'granted',
'perm-denied': pushStore.permission === 'denied',
'perm-default': pushStore.permission === 'default',
}]"
>
{{ pushStore.permission === 'granted' ? 'Granted' : pushStore.permission === 'denied' ? 'Denied' : 'Not asked' }}
</span>
</div>
<div class="push-status-row">
<span class="push-status-label">Status:</span>
<span :class="['push-sub-badge', { 'sub-active': pushStore.isSubscribed }]">
{{ pushStore.isSubscribed ? 'Subscribed' : 'Not subscribed' }}
</span>
</div>
<p v-if="pushStore.error" class="push-error">{{ pushStore.error }}</p>
<div class="actions" style="margin-top: 0.75rem;">
<button
v-if="!pushStore.isSubscribed"
class="btn-save"
@click="pushStore.subscribe()"
:disabled="pushStore.loading || pushStore.permission === 'denied'"
>
{{ pushStore.loading ? 'Enabling...' : 'Enable Notifications' }}
</button>
<button
v-else
class="btn-secondary"
@click="pushStore.unsubscribe()"
:disabled="pushStore.loading"
>
{{ pushStore.loading ? 'Disabling...' : 'Disable Notifications' }}
</button>
</div>
<p v-if="pushStore.permission === 'denied'" class="field-hint" style="margin-top: 0.5rem;">
Notifications are blocked. Allow them in your browser site settings to re-enable.
</p>
</template>
</section>
<!-- Application URL (admin) full width -->
<section v-if="authStore.isAdmin" class="settings-section full-width">
<h2>Application URL</h2>
@@ -1022,6 +1078,43 @@ function hostname(url: string): string {
max-width: 480px;
}
/* Push notifications */
.push-unsupported {
font-size: 0.875rem;
color: var(--color-text-muted);
font-style: italic;
}
.push-status-row {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.4rem;
font-size: 0.875rem;
}
.push-status-label {
color: var(--color-text-secondary);
font-weight: 500;
}
.push-permission-badge,
.push-sub-badge {
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 0.1rem 0.4rem;
border-radius: 999px;
background: color-mix(in srgb, var(--color-text-muted) 15%, transparent);
color: var(--color-text-muted);
}
.perm-granted { background: color-mix(in srgb, var(--color-success) 15%, transparent); color: var(--color-success); }
.perm-denied { background: color-mix(in srgb, var(--color-danger, #e74c3c) 15%, transparent); color: var(--color-danger, #e74c3c); }
.sub-active { background: color-mix(in srgb, var(--color-success) 15%, transparent); color: var(--color-success); }
.push-error {
font-size: 0.82rem;
color: var(--color-danger, #e74c3c);
margin: 0.25rem 0 0;
}
/* Responsive — collapse to 1 column on narrow screens */
@media (max-width: 640px) {
.settings-grid {