diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts
index 1b97f86..8a8444d 100644
--- a/src/app/api/tasks/route.ts
+++ b/src/app/api/tasks/route.ts
@@ -67,7 +67,6 @@ export async function POST(request: Request) {
assigneeId: task.assigneeId || user.id,
assigneeName: task.assigneeName || user.name,
assigneeEmail: task.assigneeEmail || user.email,
- assigneeAvatarUrl: task.assigneeAvatarUrl || user.avatarUrl,
});
}
}
@@ -84,7 +83,7 @@ export async function POST(request: Request) {
assigneeId: entry.assigneeId || undefined,
assigneeName: entry.assigneeName || undefined,
assigneeEmail: entry.assigneeEmail || undefined,
- assigneeAvatarUrl: entry.assigneeAvatarUrl || undefined,
+ assigneeAvatarUrl: undefined,
}));
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 1229de6..4d568bf 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -219,11 +219,13 @@ function KanbanDropColumn({
function KanbanTaskCard({
task,
taskTags,
+ assigneeAvatarUrl,
onOpen,
onDelete,
}: {
task: Task
taskTags: string[]
+ assigneeAvatarUrl?: string
onOpen: () => void
onDelete: () => void
}) {
@@ -307,7 +309,7 @@ function KanbanTaskCard({
@@ -1002,6 +1004,7 @@ export default function Home() {
key={task.id}
task={task}
taskTags={getTags(task)}
+ assigneeAvatarUrl={resolveAssignee(task.assigneeId)?.avatarUrl || task.assigneeAvatarUrl}
onOpen={() => router.push(`/tasks/${encodeURIComponent(task.id)}`)}
onDelete={() => deleteTask(task.id)}
/>
diff --git a/src/app/tasks/[taskId]/page.tsx b/src/app/tasks/[taskId]/page.tsx
index ecbf187..57a2c02 100644
--- a/src/app/tasks/[taskId]/page.tsx
+++ b/src/app/tasks/[taskId]/page.tsx
@@ -684,7 +684,7 @@ export default function TaskDetailPage() {
diff --git a/src/components/BacklogView.tsx b/src/components/BacklogView.tsx
index 449c366..4b7242a 100644
--- a/src/components/BacklogView.tsx
+++ b/src/components/BacklogView.tsx
@@ -1,6 +1,6 @@
"use client"
-import { useState, type ReactNode } from "react"
+import { useEffect, useState, type ReactNode } from "react"
import {
DndContext,
DragEndEvent,
@@ -42,6 +42,13 @@ const typeLabels: Record = {
plan: "📐",
}
+interface AssignableUser {
+ id: string
+ name: string
+ email?: string
+ avatarUrl?: string
+}
+
function AssigneeAvatar({ name, avatarUrl, seed }: { name?: string; avatarUrl?: string; seed?: string }) {
const displayUrl = avatarUrl || generateAvatarDataUrl(seed || name || "unassigned", name || "Unassigned")
return (
@@ -57,9 +64,11 @@ function AssigneeAvatar({ name, avatarUrl, seed }: { name?: string; avatarUrl?:
// Sortable Task Row
function SortableTaskRow({
task,
+ assigneeAvatarUrl,
onClick,
}: {
task: Task
+ assigneeAvatarUrl?: string
onClick: () => void
}) {
const {
@@ -103,13 +112,13 @@ function SortableTaskRow({
{task.comments && task.comments.length > 0 && (
💬 {task.comments.length}
)}
-
+
)
}
// Drag Overlay Item
-function DragOverlayItem({ task }: { task: Task }) {
+function DragOverlayItem({ task, assigneeAvatarUrl }: { task: Task; assigneeAvatarUrl?: string }) {
return (
@@ -120,7 +129,7 @@ function DragOverlayItem({ task }: { task: Task }) {
{task.priority}
-
+
)
}
@@ -145,6 +154,7 @@ function TaskSection({
isOpen,
onToggle,
onTaskClick,
+ resolveAssigneeAvatar,
sprintInfo,
}: {
title: string
@@ -152,6 +162,7 @@ function TaskSection({
isOpen: boolean
onToggle: () => void
onTaskClick: (task: Task) => void
+ resolveAssigneeAvatar: (task: Task) => string | undefined
sprintInfo?: { name: string; date: string; status: string }
}) {
return (
@@ -196,6 +207,7 @@ function TaskSection({
onTaskClick(task)}
/>
))
@@ -210,6 +222,7 @@ function TaskSection({
export function BacklogView() {
const router = useRouter()
+ const [assignableUsers, setAssignableUsers] = useState([])
const {
tasks,
sprints,
@@ -218,6 +231,37 @@ export function BacklogView() {
addSprint,
} = useTaskStore()
+ useEffect(() => {
+ let active = true
+ const loadUsers = async () => {
+ try {
+ const response = await fetch("/api/auth/users", { cache: "no-store" })
+ if (!response.ok) return
+ const data = await response.json()
+ if (!active || !Array.isArray(data?.users)) return
+ setAssignableUsers(
+ data.users.map((entry: { id: string; name: string; email?: string; avatarUrl?: string }) => ({
+ id: entry.id,
+ name: entry.name,
+ email: entry.email,
+ avatarUrl: entry.avatarUrl,
+ })),
+ )
+ } catch {
+ // Keep backlog usable if users lookup fails.
+ }
+ }
+ void loadUsers()
+ return () => {
+ active = false
+ }
+ }, [])
+
+ const resolveAssigneeAvatar = (task: Task) => {
+ if (!task.assigneeId) return task.assigneeAvatarUrl
+ return assignableUsers.find((user) => user.id === task.assigneeId)?.avatarUrl || task.assigneeAvatarUrl
+ }
+
const [activeId, setActiveId] = useState(null)
const [openSections, setOpenSections] = useState>({
current: true,
@@ -331,6 +375,7 @@ export function BacklogView() {
isOpen={openSections.current}
onToggle={() => toggleSection("current")}
onTaskClick={(task) => router.push(`/tasks/${encodeURIComponent(task.id)}`)}
+ resolveAssigneeAvatar={resolveAssigneeAvatar}
sprintInfo={
currentSprint
? {
@@ -362,6 +407,7 @@ export function BacklogView() {
isOpen={openSections[sprint.id] ?? false}
onToggle={() => toggleSection(sprint.id)}
onTaskClick={(task) => router.push(`/tasks/${encodeURIComponent(task.id)}`)}
+ resolveAssigneeAvatar={resolveAssigneeAvatar}
sprintInfo={{
name: sprint.name,
date: (() => {
@@ -439,12 +485,13 @@ export function BacklogView() {
isOpen={openSections.backlog}
onToggle={() => toggleSection("backlog")}
onTaskClick={(task) => router.push(`/tasks/${encodeURIComponent(task.id)}`)}
+ resolveAssigneeAvatar={resolveAssigneeAvatar}
/>
- {activeTask ? : null}
+ {activeTask ? : null}
)
diff --git a/src/lib/server/taskDb.ts b/src/lib/server/taskDb.ts
index 9d9e5ec..75fb04e 100644
--- a/src/lib/server/taskDb.ts
+++ b/src/lib/server/taskDb.ts
@@ -98,6 +98,13 @@ type SqliteDb = InstanceType;
let db: SqliteDb | null = null;
+interface UserProfileLookup {
+ id: string;
+ name: string;
+ email?: string;
+ avatarUrl?: string;
+}
+
function ensureTaskSchema(database: SqliteDb) {
const taskColumns = database.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
if (!taskColumns.some((column) => column.name === "attachments")) {
@@ -145,6 +152,32 @@ function safeParseArray(value: string | null, fallback: T[]): T[] {
}
}
+function getUserLookup(database: SqliteDb): Map {
+ const hasUsersTable = database
+ .prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'users' LIMIT 1")
+ .get() as { 1: number } | undefined;
+ if (!hasUsersTable) return new Map();
+
+ try {
+ const rows = database
+ .prepare("SELECT id, name, email, avatarUrl FROM users")
+ .all() as Array<{ id: string; name: string; email: string | null; avatarUrl: string | null }>;
+
+ const lookup = new Map();
+ for (const row of rows) {
+ lookup.set(row.id, {
+ id: row.id,
+ name: row.name,
+ email: row.email ?? undefined,
+ avatarUrl: row.avatarUrl ?? undefined,
+ });
+ }
+ return lookup;
+ } catch {
+ return new Map();
+ }
+}
+
function normalizeAttachments(attachments: unknown): TaskAttachment[] {
if (!Array.isArray(attachments)) return [];
@@ -421,6 +454,7 @@ function getDb(): SqliteDb {
export function getData(): DataStore {
const database = getDb();
+ const usersById = getUserLookup(database);
const projects = database.prepare("SELECT * FROM projects ORDER BY createdAt ASC").all() as Array<{
id: string;
@@ -486,32 +520,38 @@ export function getData(): DataStore {
projectId: sprint.projectId,
createdAt: sprint.createdAt,
})),
- tasks: tasks.map((task) => ({
- id: task.id,
- title: task.title,
- description: task.description ?? undefined,
- type: task.type,
- status: task.status,
- priority: task.priority,
- projectId: task.projectId,
- sprintId: task.sprintId ?? undefined,
- createdAt: task.createdAt,
- updatedAt: task.updatedAt,
- createdById: task.createdById ?? undefined,
- createdByName: task.createdByName ?? undefined,
- createdByAvatarUrl: task.createdByAvatarUrl ?? undefined,
- updatedById: task.updatedById ?? undefined,
- updatedByName: task.updatedByName ?? undefined,
- updatedByAvatarUrl: task.updatedByAvatarUrl ?? undefined,
- assigneeId: task.assigneeId ?? undefined,
- assigneeName: task.assigneeName ?? undefined,
- assigneeEmail: task.assigneeEmail ?? undefined,
- assigneeAvatarUrl: task.assigneeAvatarUrl ?? undefined,
- dueDate: task.dueDate ?? undefined,
- comments: normalizeComments(safeParseArray(task.comments, [])),
- tags: safeParseArray(task.tags, []),
- attachments: normalizeAttachments(safeParseArray(task.attachments, [])),
- })),
+ tasks: tasks.map((task) => {
+ const createdByUser = task.createdById ? usersById.get(task.createdById) : undefined;
+ const updatedByUser = task.updatedById ? usersById.get(task.updatedById) : undefined;
+ const assigneeUser = task.assigneeId ? usersById.get(task.assigneeId) : undefined;
+
+ return {
+ id: task.id,
+ title: task.title,
+ description: task.description ?? undefined,
+ type: task.type,
+ status: task.status,
+ priority: task.priority,
+ projectId: task.projectId,
+ sprintId: task.sprintId ?? undefined,
+ createdAt: task.createdAt,
+ updatedAt: task.updatedAt,
+ createdById: task.createdById ?? undefined,
+ createdByName: task.createdByName ?? createdByUser?.name ?? undefined,
+ createdByAvatarUrl: createdByUser?.avatarUrl ?? task.createdByAvatarUrl ?? undefined,
+ updatedById: task.updatedById ?? undefined,
+ updatedByName: task.updatedByName ?? updatedByUser?.name ?? undefined,
+ updatedByAvatarUrl: updatedByUser?.avatarUrl ?? task.updatedByAvatarUrl ?? undefined,
+ assigneeId: task.assigneeId ?? undefined,
+ assigneeName: assigneeUser?.name ?? task.assigneeName ?? undefined,
+ assigneeEmail: assigneeUser?.email ?? task.assigneeEmail ?? undefined,
+ assigneeAvatarUrl: assigneeUser?.avatarUrl ?? undefined,
+ dueDate: task.dueDate ?? undefined,
+ comments: normalizeComments(safeParseArray(task.comments, [])),
+ tags: safeParseArray(task.tags, []),
+ attachments: normalizeAttachments(safeParseArray(task.attachments, [])),
+ };
+ }),
lastUpdated: getLastUpdated(database),
};
}