Add invitation system, table of contents, actionable dashboard, and settings improvements
Invitation system (Phase 5.7): - invitation_tokens table (migration 0012) with SHA256-hashed tokens, 7-day expiry - Admin CRUD endpoints: POST/GET/DELETE /api/admin/invitations - Branded invitation email with registration link - /register-invite frontend view with token validation and account creation - Admin UI: invite form + pending invitations table with revoke - Configurable base URL setting for email links (replaces hardcoded Config.BASE_URL) Table of contents + markdown improvements (Phase 5.8): - TableOfContents component: sticky sidebar, heading parsing, smooth-scroll - Custom marked renderer adds id attributes to headings for anchor links - stripFirstLineTags() prevents leading #tags from rendering as headings - NoteViewerView/TaskViewerView flex layout with TOC sidebar (hidden ≤1200px) - Model catalog refresh: added llama3.2/3.3, gemma3, qwen3, phi4, deepseek-r1, qwen2.5-coder, dolphin3; added Reasoning category; removed discontinued models - Settings model section split into Installed/Available tabs Actionable dashboard (Phase 5.9): - due_before/due_after query params on /api/tasks (exclusive < / inclusive >=) - HomeView rewritten: overdue (red accent), due today, in progress, chats, notes - 5 parallel API calls via Promise.allSettled - Client-side done filtering and in-progress deduplication - Task sections hidden when empty; status toggle removes done tasks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+122
-43
@@ -13,47 +13,96 @@ import { useChatStore } from "@/stores/chat";
|
||||
|
||||
const router = useRouter();
|
||||
const recentNotes = ref<Note[]>([]);
|
||||
const recentTasks = ref<Task[]>([]);
|
||||
const overdueTasks = ref<Task[]>([]);
|
||||
const dueTodayTasks = ref<Task[]>([]);
|
||||
const inProgressTasks = ref<Task[]>([]);
|
||||
const recentChats = ref<Conversation[]>([]);
|
||||
const loading = ref(true);
|
||||
const tasksStore = useTasksStore();
|
||||
|
||||
function todayStr(): string {
|
||||
const d = new Date();
|
||||
return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
|
||||
}
|
||||
|
||||
function tomorrowStr(): string {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() + 1);
|
||||
return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const notesData = await apiGet<NoteListResponse>(
|
||||
"/api/notes?sort=updated_at&order=desc&limit=5"
|
||||
);
|
||||
recentNotes.value = notesData.notes;
|
||||
} catch (e) {
|
||||
console.error("Failed to load recent notes:", e);
|
||||
const today = todayStr();
|
||||
const tomorrow = tomorrowStr();
|
||||
|
||||
const [notesRes, overdueRes, dueTodayRes, inProgressRes, chatsRes] =
|
||||
await Promise.allSettled([
|
||||
apiGet<NoteListResponse>("/api/notes?sort=updated_at&order=desc&limit=5"),
|
||||
apiGet<TaskListResponse>(
|
||||
`/api/tasks?due_before=${today}&sort=due_date&order=asc&limit=15`
|
||||
),
|
||||
apiGet<TaskListResponse>(
|
||||
`/api/tasks?due_after=${today}&due_before=${tomorrow}&sort=priority&order=desc&limit=10`
|
||||
),
|
||||
apiGet<TaskListResponse>(
|
||||
`/api/tasks?status=in_progress&sort=updated_at&order=desc&limit=5`
|
||||
),
|
||||
apiGet<{ conversations: Conversation[]; total: number }>(
|
||||
"/api/chat/conversations?limit=3&offset=0"
|
||||
),
|
||||
]);
|
||||
|
||||
if (notesRes.status === "fulfilled") {
|
||||
recentNotes.value = notesRes.value.notes;
|
||||
}
|
||||
if (chatsRes.status === "fulfilled") {
|
||||
recentChats.value = chatsRes.value.conversations;
|
||||
}
|
||||
|
||||
try {
|
||||
const tasksData = await apiGet<TaskListResponse>(
|
||||
"/api/tasks?sort=updated_at&order=desc&limit=5"
|
||||
// Filter out done tasks from overdue and due-today
|
||||
if (overdueRes.status === "fulfilled") {
|
||||
overdueTasks.value = overdueRes.value.tasks.filter(
|
||||
(t) => t.status !== "done"
|
||||
);
|
||||
}
|
||||
if (dueTodayRes.status === "fulfilled") {
|
||||
dueTodayTasks.value = dueTodayRes.value.tasks.filter(
|
||||
(t) => t.status !== "done"
|
||||
);
|
||||
recentTasks.value = tasksData.tasks;
|
||||
} catch (e) {
|
||||
console.error("Failed to load recent tasks:", e);
|
||||
}
|
||||
|
||||
try {
|
||||
const chatData = await apiGet<{ conversations: Conversation[]; total: number }>(
|
||||
"/api/chat/conversations?limit=3&offset=0"
|
||||
// Deduplicate in-progress against overdue and due-today
|
||||
if (inProgressRes.status === "fulfilled") {
|
||||
const seen = new Set<number>();
|
||||
for (const t of overdueTasks.value) seen.add(t.id);
|
||||
for (const t of dueTodayTasks.value) seen.add(t.id);
|
||||
inProgressTasks.value = inProgressRes.value.tasks.filter(
|
||||
(t) => !seen.has(t.id)
|
||||
);
|
||||
recentChats.value = chatData.conversations;
|
||||
} catch (e) {
|
||||
console.error("Failed to load recent chats:", e);
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
function removeFromAllLists(id: number) {
|
||||
overdueTasks.value = overdueTasks.value.filter((t) => t.id !== id);
|
||||
dueTodayTasks.value = dueTodayTasks.value.filter((t) => t.id !== id);
|
||||
inProgressTasks.value = inProgressTasks.value.filter((t) => t.id !== id);
|
||||
}
|
||||
|
||||
function onStatusToggle(id: number, status: TaskStatus) {
|
||||
tasksStore.patchStatus(id, status).then((updated) => {
|
||||
const idx = recentTasks.value.findIndex((t) => t.id === updated.id);
|
||||
if (idx !== -1) {
|
||||
recentTasks.value[idx] = updated;
|
||||
if (updated.status === "done") {
|
||||
removeFromAllLists(updated.id);
|
||||
} else {
|
||||
// Update in whichever list contains it
|
||||
for (const list of [overdueTasks, dueTodayTasks, inProgressTasks]) {
|
||||
const idx = list.value.findIndex((t) => t.id === updated.id);
|
||||
if (idx !== -1) {
|
||||
list.value[idx] = updated;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -73,6 +122,51 @@ async function newChat() {
|
||||
<p v-if="loading" class="loading">Loading...</p>
|
||||
|
||||
<template v-else>
|
||||
<section v-if="overdueTasks.length" class="section section-overdue">
|
||||
<div class="section-header">
|
||||
<h2>Overdue</h2>
|
||||
<router-link to="/tasks" class="see-all">See all</router-link>
|
||||
</div>
|
||||
<div class="cards">
|
||||
<TaskCard
|
||||
v-for="task in overdueTasks"
|
||||
:key="task.id"
|
||||
:task="task"
|
||||
@status-toggle="onStatusToggle"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="dueTodayTasks.length" class="section">
|
||||
<div class="section-header">
|
||||
<h2>Due Today</h2>
|
||||
<router-link to="/tasks" class="see-all">See all</router-link>
|
||||
</div>
|
||||
<div class="cards">
|
||||
<TaskCard
|
||||
v-for="task in dueTodayTasks"
|
||||
:key="task.id"
|
||||
:task="task"
|
||||
@status-toggle="onStatusToggle"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="inProgressTasks.length" class="section">
|
||||
<div class="section-header">
|
||||
<h2>In Progress</h2>
|
||||
<router-link to="/tasks" class="see-all">See all</router-link>
|
||||
</div>
|
||||
<div class="cards">
|
||||
<TaskCard
|
||||
v-for="task in inProgressTasks"
|
||||
:key="task.id"
|
||||
:task="task"
|
||||
@status-toggle="onStatusToggle"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>Recent Chats</h2>
|
||||
@@ -97,7 +191,7 @@ async function newChat() {
|
||||
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>Recent Notes</h2>
|
||||
<h2>Recently Edited</h2>
|
||||
<router-link to="/notes" class="see-all">See all</router-link>
|
||||
</div>
|
||||
<div v-if="recentNotes.length" class="cards">
|
||||
@@ -112,25 +206,6 @@ async function newChat() {
|
||||
<router-link to="/notes/new" class="btn-cta">+ New Note</router-link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>Recent Tasks</h2>
|
||||
<router-link to="/tasks" class="see-all">See all</router-link>
|
||||
</div>
|
||||
<div v-if="recentTasks.length" class="cards">
|
||||
<TaskCard
|
||||
v-for="task in recentTasks"
|
||||
:key="task.id"
|
||||
:task="task"
|
||||
@status-toggle="onStatusToggle"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<p class="empty-text">No tasks yet.</p>
|
||||
<router-link to="/tasks/new" class="btn-cta">+ New Task</router-link>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</main>
|
||||
</template>
|
||||
@@ -150,6 +225,10 @@ async function newChat() {
|
||||
.section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.section-overdue {
|
||||
border-left: 3px solid var(--color-overdue, #e74c3c);
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
Reference in New Issue
Block a user