- {(m.displayName || 'U').slice(0, 1).toUpperCase()}
-
{m.displayName} {isSelf && (Du)}
diff --git a/frontend/src/features/triage/TasksView.tsx b/frontend/src/features/triage/TasksView.tsx
index 4d319be..f198564 100644
--- a/frontend/src/features/triage/TasksView.tsx
+++ b/frontend/src/features/triage/TasksView.tsx
@@ -1249,8 +1249,12 @@ export const TasksView: React.FC = ({
return (
{otherMembers.slice(0, 2).map((m) => (
-
- {getInitials(m.displayName || m.email)[0]}
+
+ {m.avatarUrl ? (
+
+ ) : (
+ getInitials(m.displayName || m.email)[0]
+ )}
))}
@@ -1563,6 +1567,7 @@ export const TasksView: React.FC = ({
targetId: sh.targetId,
targetName: sh.targetName || (sh.targetType === 'team' ? 'Gruppe' : sh.email || 'Benutzer'),
targetEmail: sh.email,
+ avatarUrl: sh.avatarUrl,
permission: 'editor' as const,
role: 'editor',
createdAt: sh.createdAt,
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 492e6b8..c11c321 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -163,6 +163,7 @@ export const api = {
displayName?: string;
email?: string;
password?: string;
+ avatarUrl?: string;
alarmChannels?: string[];
alarmEmail?: string;
discordWebhookUrl?: string;
diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts
index ef62c94..4d2ff16 100644
--- a/frontend/src/lib/types.ts
+++ b/frontend/src/lib/types.ts
@@ -14,6 +14,7 @@ export interface User {
email: string;
username?: string;
displayName: string;
+ avatarUrl?: string;
role: UserRole;
isActive: boolean;
alarmChannels?: string[];
@@ -79,6 +80,7 @@ export interface TaskListShare {
targetId: string;
targetName?: string;
email?: string;
+ avatarUrl?: string;
createdAt: string;
}
@@ -86,6 +88,7 @@ export interface TaskListMember {
userId: string;
displayName: string;
email: string;
+ avatarUrl?: string;
role: string;
}
@@ -132,6 +135,7 @@ export interface TeamMember {
userId: string;
displayName: string;
email: string;
+ avatarUrl?: string;
role: 'admin' | 'member';
joinedAt: string;
}
@@ -240,6 +244,7 @@ export interface SpaceMember {
userId: string;
displayName: string;
email: string;
+ avatarUrl?: string;
role: 'owner' | 'editor' | 'viewer';
joinedAt: string;
}
@@ -250,6 +255,7 @@ export interface UnifiedShare {
targetId: string;
targetName?: string;
targetEmail?: string;
+ avatarUrl?: string;
permission?: 'editor' | 'viewer';
role?: string;
createdBy?: string;
diff --git a/internal/api/server.go b/internal/api/server.go
index ecebb35..5ad6a2f 100644
--- a/internal/api/server.go
+++ b/internal/api/server.go
@@ -1505,6 +1505,7 @@ func (s *Server) handleUpdateUserSettings(w http.ResponseWriter, r *http.Request
DisplayName *string `json:"displayName"`
Email *string `json:"email"`
Password *string `json:"password"`
+ AvatarURL *string `json:"avatarUrl"`
AlarmChannels *[]string `json:"alarmChannels"`
AlarmEmail *string `json:"alarmEmail"`
DiscordWebhookURL *string `json:"discordWebhookUrl"`
@@ -1534,6 +1535,7 @@ func (s *Server) handleUpdateUserSettings(w http.ResponseWriter, r *http.Request
req.DiscordWebhookURL,
req.CustomWebhookURL,
req.NotifyBeforeMinutes,
+ req.AvatarURL,
)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
diff --git a/internal/domain/models.go b/internal/domain/models.go
index 4934766..fcf16f6 100644
--- a/internal/domain/models.go
+++ b/internal/domain/models.go
@@ -98,6 +98,7 @@ type User struct {
Username string `json:"username,omitempty"`
PasswordHash string `json:"-"`
DisplayName string `json:"displayName"`
+ AvatarURL string `json:"avatarUrl,omitempty"`
Role string `json:"role"` // admin | member
IsActive bool `json:"isActive"`
AlarmChannels []string `json:"alarmChannels"` // email | discord | webhook
@@ -179,6 +180,7 @@ type TaskListShare struct {
TargetID string `json:"targetId"`
TargetName string `json:"targetName,omitempty"`
Email string `json:"email,omitempty"`
+ AvatarURL string `json:"avatarUrl,omitempty"`
CreatedAt string `json:"createdAt"`
}
@@ -186,6 +188,7 @@ type TaskListMember struct {
UserID string `json:"userId"`
DisplayName string `json:"displayName"`
Email string `json:"email"`
+ AvatarURL string `json:"avatarUrl,omitempty"`
Role string `json:"role"` // owner | member
}
@@ -238,6 +241,7 @@ type TeamMember struct {
UserID string `json:"userId"`
DisplayName string `json:"displayName"`
Email string `json:"email"`
+ AvatarURL string `json:"avatarUrl,omitempty"`
Role string `json:"role"` // admin | member
JoinedAt string `json:"joinedAt"`
}
diff --git a/internal/storage/sqlite.go b/internal/storage/sqlite.go
index 819cf75..a748531 100644
--- a/internal/storage/sqlite.go
+++ b/internal/storage/sqlite.go
@@ -81,6 +81,7 @@ func (s *Store) migrate() error {
discord_webhook_url TEXT NOT NULL DEFAULT '',
custom_webhook_url TEXT NOT NULL DEFAULT '',
notify_before_minutes INTEGER NOT NULL DEFAULT 30,
+ avatar_url TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL
);
@@ -313,6 +314,7 @@ func (s *Store) migrate() error {
"ALTER TABLE users ADD COLUMN discord_webhook_url TEXT NOT NULL DEFAULT '';",
"ALTER TABLE users ADD COLUMN custom_webhook_url TEXT NOT NULL DEFAULT '';",
"ALTER TABLE users ADD COLUMN notify_before_minutes INTEGER NOT NULL DEFAULT 30;",
+ "ALTER TABLE users ADD COLUMN avatar_url TEXT NOT NULL DEFAULT '';",
"ALTER TABLE thoughts ADD COLUMN task_list_id TEXT NOT NULL DEFAULT '';",
"ALTER TABLE thoughts ADD COLUMN kanban_status TEXT NOT NULL DEFAULT 'todo';",
@@ -486,7 +488,7 @@ func (s *Store) EnsureDefaultUser() (*domain.User, error) {
// --- USERS ---
-const userSelectCols = `id, email, username, password_hash, display_name, role, is_active, alarm_channels, alarm_email, discord_webhook_url, custom_webhook_url, notify_before_minutes, created_at`
+const userSelectCols = `id, email, username, password_hash, display_name, role, is_active, alarm_channels, alarm_email, discord_webhook_url, custom_webhook_url, notify_before_minutes, avatar_url, created_at`
func parseAlarmChannels(raw string) []string {
parts := strings.Split(raw, ",")
@@ -509,7 +511,7 @@ func scanUser(row interface{ Scan(dest ...any) error }) (*domain.User, error) {
err := row.Scan(
&u.ID, &u.Email, &u.Username, &u.PasswordHash, &u.DisplayName,
&u.Role, &isActiveInt, &channelsRaw, &u.AlarmEmail,
- &u.DiscordWebhookURL, &u.CustomWebhookURL, &u.NotifyBeforeMinutes, &u.CreatedAt,
+ &u.DiscordWebhookURL, &u.CustomWebhookURL, &u.NotifyBeforeMinutes, &u.AvatarURL, &u.CreatedAt,
)
if err != nil {
return nil, err
@@ -559,11 +561,12 @@ func (s *Store) CreateUserWithUsername(email, username, passwordHash, displayNam
DiscordWebhookURL: "",
CustomWebhookURL: "",
NotifyBeforeMinutes: 30,
+ AvatarURL: "",
CreatedAt: domain.NowISO(),
}
_, err := s.db.Exec(
- `INSERT INTO users (id, email, username, password_hash, display_name, role, is_active, alarm_channels, alarm_email, discord_webhook_url, custom_webhook_url, notify_before_minutes, created_at)
- VALUES (?, ?, ?, ?, ?, ?, 1, '', ?, '', '', 30, ?)`,
+ `INSERT INTO users (id, email, username, password_hash, display_name, role, is_active, alarm_channels, alarm_email, discord_webhook_url, custom_webhook_url, notify_before_minutes, avatar_url, created_at)
+ VALUES (?, ?, ?, ?, ?, ?, 1, '', ?, '', '', 30, '', ?)`,
u.ID, u.Email, u.Username, u.PasswordHash, u.DisplayName, u.Role, u.AlarmEmail, u.CreatedAt,
)
if err != nil {
@@ -714,7 +717,7 @@ func (s *Store) UpdateUser(
discordWebhookURL *string,
notifyBeforeMinutes *int,
) (*domain.User, error) {
- return s.UpdateUserFull(userID, displayName, email, role, isActive, nil, nil, discordWebhookURL, nil, notifyBeforeMinutes)
+ return s.UpdateUserFull(userID, displayName, email, role, isActive, nil, nil, discordWebhookURL, nil, notifyBeforeMinutes, nil)
}
func (s *Store) UpdateUserFull(
@@ -724,6 +727,7 @@ func (s *Store) UpdateUserFull(
alarmChannels *[]string,
alarmEmail, discordWebhookURL, customWebhookURL *string,
notifyBeforeMinutes *int,
+ avatarURL *string,
) (*domain.User, error) {
u, err := s.GetUserByID(userID)
if err != nil {
@@ -756,14 +760,17 @@ func (s *Store) UpdateUserFull(
if notifyBeforeMinutes != nil {
u.NotifyBeforeMinutes = *notifyBeforeMinutes
}
+ if avatarURL != nil {
+ u.AvatarURL = strings.TrimSpace(*avatarURL)
+ }
activeInt := 0
if u.IsActive {
activeInt = 1
}
channelsStr := strings.Join(u.AlarmChannels, ",")
_, err = s.db.Exec(
- `UPDATE users SET display_name = ?, email = ?, role = ?, is_active = ?, alarm_channels = ?, alarm_email = ?, discord_webhook_url = ?, custom_webhook_url = ?, notify_before_minutes = ? WHERE id = ?`,
- u.DisplayName, u.Email, u.Role, activeInt, channelsStr, u.AlarmEmail, u.DiscordWebhookURL, u.CustomWebhookURL, u.NotifyBeforeMinutes, userID,
+ `UPDATE users SET display_name = ?, email = ?, role = ?, is_active = ?, alarm_channels = ?, alarm_email = ?, discord_webhook_url = ?, custom_webhook_url = ?, notify_before_minutes = ?, avatar_url = ? WHERE id = ?`,
+ u.DisplayName, u.Email, u.Role, activeInt, channelsStr, u.AlarmEmail, u.DiscordWebhookURL, u.CustomWebhookURL, u.NotifyBeforeMinutes, u.AvatarURL, userID,
)
if err != nil {
return nil, err
@@ -2196,7 +2203,7 @@ func (s *Store) DeleteDocShare(currentUserID, docID, targetID string) error {
func (s *Store) GetCollaborators(currentUserID string) ([]domain.User, error) {
rows, err := s.db.Query(
- `SELECT id, email, display_name, role, is_active, created_at FROM users WHERE id != ? AND is_active = 1 ORDER BY display_name ASC`,
+ `SELECT id, email, display_name, role, is_active, avatar_url, created_at FROM users WHERE id != ? AND is_active = 1 ORDER BY display_name ASC`,
currentUserID,
)
if err != nil {
@@ -2208,7 +2215,7 @@ func (s *Store) GetCollaborators(currentUserID string) ([]domain.User, error) {
for rows.Next() {
var u domain.User
var activeInt int
- if err := rows.Scan(&u.ID, &u.Email, &u.DisplayName, &u.Role, &activeInt, &u.CreatedAt); err == nil {
+ if err := rows.Scan(&u.ID, &u.Email, &u.DisplayName, &u.Role, &activeInt, &u.AvatarURL, &u.CreatedAt); err == nil {
u.IsActive = activeInt == 1
users = append(users, u)
}
@@ -2936,7 +2943,7 @@ func (s *Store) GetTaskListShares(taskListID string) ([]domain.TaskListShare, er
if shares[i].TargetType == "team" {
_ = s.db.QueryRow(`SELECT name FROM teams WHERE id = ?`, shares[i].TargetID).Scan(&shares[i].TargetName)
} else {
- _ = s.db.QueryRow(`SELECT display_name, email FROM users WHERE id = ?`, shares[i].TargetID).Scan(&shares[i].TargetName, &shares[i].Email)
+ _ = s.db.QueryRow(`SELECT display_name, email, avatar_url FROM users WHERE id = ?`, shares[i].TargetID).Scan(&shares[i].TargetName, &shares[i].Email, &shares[i].AvatarURL)
}
}
return shares, nil
@@ -2946,25 +2953,26 @@ func (s *Store) getMembersForTaskList(tl *domain.TaskList) []domain.TaskListMemb
memberMap := make(map[string]domain.TaskListMember)
// 1. Owner
- var ownerName, ownerEmail string
- _ = s.db.QueryRow(`SELECT display_name, email FROM users WHERE id = ?`, tl.UserID).Scan(&ownerName, &ownerEmail)
+ var ownerName, ownerEmail, ownerAvatar string
+ _ = s.db.QueryRow(`SELECT display_name, email, avatar_url FROM users WHERE id = ?`, tl.UserID).Scan(&ownerName, &ownerEmail, &ownerAvatar)
memberMap[tl.UserID] = domain.TaskListMember{
UserID: tl.UserID,
DisplayName: ownerName,
Email: ownerEmail,
+ AvatarURL: ownerAvatar,
Role: "owner",
}
// 2. Team members if tl.TeamID != ""
if tl.TeamID != "" {
rows, err := s.db.Query(
- `SELECT u.id, u.display_name, u.email FROM team_members tm JOIN users u ON u.id = tm.user_id WHERE tm.team_id = ?`,
+ `SELECT u.id, u.display_name, u.email, u.avatar_url FROM team_members tm JOIN users u ON u.id = tm.user_id WHERE tm.team_id = ?`,
tl.TeamID,
)
if err == nil {
for rows.Next() {
var m domain.TaskListMember
- if err := rows.Scan(&m.UserID, &m.DisplayName, &m.Email); err == nil {
+ if err := rows.Scan(&m.UserID, &m.DisplayName, &m.Email, &m.AvatarURL); err == nil {
m.Role = "member"
memberMap[m.UserID] = m
}
@@ -2977,13 +2985,13 @@ func (s *Store) getMembersForTaskList(tl *domain.TaskList) []domain.TaskListMemb
for _, sh := range tl.Shares {
if sh.TargetType == "team" {
rows, err := s.db.Query(
- `SELECT u.id, u.display_name, u.email FROM team_members tm JOIN users u ON u.id = tm.user_id WHERE tm.team_id = ?`,
+ `SELECT u.id, u.display_name, u.email, u.avatar_url FROM team_members tm JOIN users u ON u.id = tm.user_id WHERE tm.team_id = ?`,
sh.TargetID,
)
if err == nil {
for rows.Next() {
var m domain.TaskListMember
- if err := rows.Scan(&m.UserID, &m.DisplayName, &m.Email); err == nil {
+ if err := rows.Scan(&m.UserID, &m.DisplayName, &m.Email, &m.AvatarURL); err == nil {
m.Role = "member"
memberMap[m.UserID] = m
}
@@ -2992,7 +3000,7 @@ func (s *Store) getMembersForTaskList(tl *domain.TaskList) []domain.TaskListMemb
}
} else if sh.TargetType == "user" {
var m domain.TaskListMember
- if err := s.db.QueryRow(`SELECT id, display_name, email FROM users WHERE id = ?`, sh.TargetID).Scan(&m.UserID, &m.DisplayName, &m.Email); err == nil {
+ if err := s.db.QueryRow(`SELECT id, display_name, email, avatar_url FROM users WHERE id = ?`, sh.TargetID).Scan(&m.UserID, &m.DisplayName, &m.Email, &m.AvatarURL); err == nil {
m.Role = "member"
memberMap[m.UserID] = m
}
@@ -3466,7 +3474,7 @@ func (s *Store) RemoveTeamMember(teamID, userID string) error {
func (s *Store) GetTeamMembers(teamID string) ([]domain.TeamMember, error) {
rows, err := s.db.Query(
- `SELECT tm.team_id, tm.user_id, u.display_name, u.email, tm.role, tm.joined_at
+ `SELECT tm.team_id, tm.user_id, u.display_name, u.email, u.avatar_url, tm.role, tm.joined_at
FROM team_members tm
JOIN users u ON u.id = tm.user_id
WHERE tm.team_id = ?
@@ -3481,7 +3489,7 @@ func (s *Store) GetTeamMembers(teamID string) ([]domain.TeamMember, error) {
members := make([]domain.TeamMember, 0)
for rows.Next() {
var m domain.TeamMember
- if err := rows.Scan(&m.TeamID, &m.UserID, &m.DisplayName, &m.Email, &m.Role, &m.JoinedAt); err == nil {
+ if err := rows.Scan(&m.TeamID, &m.UserID, &m.DisplayName, &m.Email, &m.AvatarURL, &m.Role, &m.JoinedAt); err == nil {
members = append(members, m)
}
}