From 9dc91b08a9479bddf1543144462ae33c9d54282d Mon Sep 17 00:00:00 2001 From: Maurice Date: Wed, 8 Apr 2026 18:09:18 +0200 Subject: [PATCH 1/4] fix: prevent note modal from closing on outside click Removed backdrop click-to-close on the note form modal so edits are not lost when clicking outside or switching browser tabs. Fixes #480 --- client/src/components/Collab/CollabNotes.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/components/Collab/CollabNotes.tsx b/client/src/components/Collab/CollabNotes.tsx index 3f8aef70..66a4dbd7 100644 --- a/client/src/components/Collab/CollabNotes.tsx +++ b/client/src/components/Collab/CollabNotes.tsx @@ -313,7 +313,6 @@ function NoteFormModal({ onClose, onSubmit, onDeleteFile, existingCategories, ca padding: 16, fontFamily: FONT, }} - onClick={onClose} >
Date: Wed, 8 Apr 2026 18:17:08 +0200 Subject: [PATCH 2/4] fix: missing avatar URLs in notifications, admin panel, and budget - Notifications: map raw avatar filename to /uploads/avatars/ URL in getNotifications, createNotification broadcasts, and respond handler - Admin listUsers: include avatar field in SELECT and map to avatar_url - Admin page: render actual avatar image instead of initial letter only - Budget loadItemMembers: map avatar to avatar_url (fixed in prior commit) Fixes #507 --- client/src/pages/AdminPage.tsx | 11 ++++++++--- server/src/services/adminService.ts | 5 +++-- server/src/services/inAppNotifications.ts | 20 +++++++++++++++----- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/client/src/pages/AdminPage.tsx b/client/src/pages/AdminPage.tsx index c6f5d516..051ce84c 100644 --- a/client/src/pages/AdminPage.tsx +++ b/client/src/pages/AdminPage.tsx @@ -30,6 +30,7 @@ interface AdminUser { last_login?: string | null online?: boolean oidc_issuer?: string | null + avatar_url?: string | null } interface AdminStats { @@ -605,9 +606,13 @@ export default function AdminPage(): React.ReactElement {
-
- {u.username.charAt(0).toUpperCase()} -
+ {u.avatar_url ? ( + {u.username} + ) : ( +
+ {u.username.charAt(0).toUpperCase()} +
+ )}
diff --git a/server/src/services/adminService.ts b/server/src/services/adminService.ts index cdd8dbaa..9ca96604 100644 --- a/server/src/services/adminService.ts +++ b/server/src/services/adminService.ts @@ -40,8 +40,8 @@ export const isDocker = (() => { export function listUsers() { const users = db.prepare( - 'SELECT id, username, email, role, created_at, updated_at, last_login FROM users ORDER BY created_at DESC' - ).all() as Pick[]; + 'SELECT id, username, email, role, avatar, created_at, updated_at, last_login FROM users ORDER BY created_at DESC' + ).all() as (Pick & { avatar?: string | null })[]; let onlineUserIds = new Set(); try { const { getOnlineUserIds } = require('../websocket'); @@ -49,6 +49,7 @@ export function listUsers() { } catch { /* */ } return users.map(u => ({ ...u, + avatar_url: u.avatar ? `/uploads/avatars/${u.avatar}` : null, created_at: utcSuffix(u.created_at), updated_at: utcSuffix(u.updated_at as string), last_login: utcSuffix(u.last_login), diff --git a/server/src/services/inAppNotifications.ts b/server/src/services/inAppNotifications.ts index 65649155..1b09ed83 100644 --- a/server/src/services/inAppNotifications.ts +++ b/server/src/services/inAppNotifications.ts @@ -159,7 +159,7 @@ function createNotification(input: NotificationInput): number[] { notification: { ...row, sender_username: sender?.username ?? null, - sender_avatar: sender?.avatar ?? null, + sender_avatar: sender?.avatar ? `/uploads/avatars/${sender.avatar}` : null, }, }); } @@ -219,7 +219,7 @@ export function createNotificationForRecipient( notification: { ...row, sender_username: sender?.username ?? null, - sender_avatar: sender?.avatar ?? null, + sender_avatar: sender?.avatar ? `/uploads/avatars/${sender.avatar}` : null, }, }); @@ -249,7 +249,12 @@ function getNotifications( const { total } = db.prepare(`SELECT COUNT(*) as total FROM notifications ${wherePlain}`).get(userId) as { total: number }; const { unread_count } = db.prepare('SELECT COUNT(*) as unread_count FROM notifications WHERE recipient_id = ? AND is_read = 0').get(userId) as { unread_count: number }; - return { notifications: rows, total, unread_count }; + const mapped = rows.map(r => ({ + ...r, + sender_avatar: r.sender_avatar ? `/uploads/avatars/${r.sender_avatar}` : null, + })); + + return { notifications: mapped, total, unread_count }; } function getUnreadCount(userId: number): number { @@ -326,9 +331,14 @@ async function respondToBoolean( WHERE n.id = ? `).get(notificationId) as NotificationRow; - broadcastToUser(userId, { type: 'notification:updated', notification: updated }); + const mappedUpdated = { + ...updated, + sender_avatar: updated.sender_avatar ? `/uploads/avatars/${updated.sender_avatar}` : null, + }; - return { success: true, notification: updated }; + broadcastToUser(userId, { type: 'notification:updated', notification: mappedUpdated }); + + return { success: true, notification: mappedUpdated }; } export { From 009b9f838aec86a818261dcf8241591af1148157 Mon Sep 17 00:00:00 2001 From: Maurice Date: Wed, 8 Apr 2026 18:36:51 +0200 Subject: [PATCH 3/4] feat: add download button to all file views Adds a dedicated download button (blob-based, works on iOS WebApp) to file cards, file preview modal, and image lightbox. Previously only "open in tab" was available which doesn't work for non-browser file types like .gpx on iOS. Fixes #462 --- client/src/components/Files/FileManager.tsx | 31 ++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/client/src/components/Files/FileManager.tsx b/client/src/components/Files/FileManager.tsx index dbaefa73..4295c46a 100644 --- a/client/src/components/Files/FileManager.tsx +++ b/client/src/components/Files/FileManager.tsx @@ -1,7 +1,7 @@ import ReactDOM from 'react-dom' import { useState, useCallback, useRef, useEffect } from 'react' import { useDropzone } from 'react-dropzone' -import { Upload, Trash2, ExternalLink, X, FileText, FileImage, File, MapPin, Ticket, StickyNote, Star, RotateCcw, Pencil, Check, ChevronLeft, ChevronRight } from 'lucide-react' +import { Upload, Trash2, ExternalLink, Download, X, FileText, FileImage, File, MapPin, Ticket, StickyNote, Star, RotateCcw, Pencil, Check, ChevronLeft, ChevronRight } from 'lucide-react' import { useToast } from '../shared/Toast' import { useTranslation } from '../../i18n' import { filesApi } from '../../api/client' @@ -30,6 +30,18 @@ function formatSize(bytes) { return `${(bytes / 1024 / 1024).toFixed(1)} MB` } +async function triggerDownload(url: string, filename: string) { + const authUrl = await getAuthUrl(url, 'download') + const res = await fetch(authUrl) + const blob = await res.blob() + const a = document.createElement('a') + a.href = URL.createObjectURL(blob) + a.download = filename + document.body.appendChild(a) + a.click() + setTimeout(() => { URL.revokeObjectURL(a.href); a.remove() }, 100) +} + function formatDateWithLocale(dateStr, locale) { if (!dateStr) return '' try { @@ -113,6 +125,12 @@ function ImageLightbox({ files, initialIndex, onClose }: ImageLightboxProps) { title={t('files.openTab')}> + @@ -514,6 +532,10 @@ export default function FileManager({ files = [], onUpload, onDelete, onUpdate, onMouseEnter={e => e.currentTarget.style.color = 'var(--text-primary)'} onMouseLeave={e => e.currentTarget.style.color = 'var(--text-faint)'}> + {can('file_delete', trip) && +