fix: Resolve branding display issues and invitation parsing errors (v2.2.1) (#86)

Fixes #84, Fixes #85
  - Fix uploads proxy routing in nginx and vite dev server
  - Fix logo/favicon state handling in BrandingPage
  - Fix invitation API response field transformation (snake_case → camelCase)  
  - Add hide_powered_by to public settings API
  - Mark Multiple Administrators as implemented in roadmap
  - Bump version to 2.2.1
This commit is contained in:
Paul Nothaft
2026-01-08 14:01:21 +01:00
committed by GitHub
10 changed files with 55 additions and 30 deletions
+12 -12
View File
@@ -45,14 +45,14 @@ jobs:
- name: Determine build platforms - name: Determine build platforms
id: platforms id: platforms
run: | run: |
# For PRs, build only amd64 to avoid QEMU emulation issues with Sharp # Only build ARM64 for tagged releases (v*.*.*)
# For main/develop/tags, build multi-arch # QEMU emulation is too slow/unreliable for npm operations on regular builds
if [[ "${{ github.event_name }}" == "pull_request" ]]; then if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
echo "platforms=linux/amd64" >> $GITHUB_OUTPUT
echo "skip_qemu=true" >> $GITHUB_OUTPUT
else
echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
echo "skip_qemu=false" >> $GITHUB_OUTPUT echo "skip_qemu=false" >> $GITHUB_OUTPUT
else
echo "platforms=linux/amd64" >> $GITHUB_OUTPUT
echo "skip_qemu=true" >> $GITHUB_OUTPUT
fi fi
- name: Set up QEMU - name: Set up QEMU
@@ -142,14 +142,14 @@ jobs:
- name: Determine build platforms - name: Determine build platforms
id: platforms id: platforms
run: | run: |
# For PRs, build only amd64 to avoid QEMU emulation issues # Only build ARM64 for tagged releases (v*.*.*)
# For main/develop/tags, build multi-arch # QEMU emulation is too slow/unreliable for npm operations on regular builds
if [[ "${{ github.event_name }}" == "pull_request" ]]; then if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
echo "platforms=linux/amd64" >> $GITHUB_OUTPUT
echo "skip_qemu=true" >> $GITHUB_OUTPUT
else
echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
echo "skip_qemu=false" >> $GITHUB_OUTPUT echo "skip_qemu=false" >> $GITHUB_OUTPUT
else
echo "platforms=linux/amd64" >> $GITHUB_OUTPUT
echo "skip_qemu=true" >> $GITHUB_OUTPUT
fi fi
- name: Set up QEMU - name: Set up QEMU
+1 -1
View File
@@ -248,7 +248,7 @@ These features are currently in beta testing and may have limited functionality
| **Face Recognition** | AI-powered face detection to help guests find their photos and create automatic person-based albums | Low | 🔄 Open | | **Face Recognition** | AI-powered face detection to help guests find their photos and create automatic person-based albums | Low | 🔄 Open |
| **Gallery Feedback** | Allow guests to like, rate, and comment on photos with admin notifications and moderation | Medium | ✅ Implemented | | **Gallery Feedback** | Allow guests to like, rate, and comment on photos with admin notifications and moderation | Medium | ✅ Implemented |
| **Video Support** | Upload and display videos alongside photos in galleries with streaming support | Low | ✅ Implemented | | **Video Support** | Upload and display videos alongside photos in galleries with streaming support | Low | ✅ Implemented |
| **Multiple Administrators** | Support for multiple admin accounts with role-based permissions and activity tracking | Low | 📋 Planned | | **Multiple Administrators** | Support for multiple admin accounts with role-based permissions and activity tracking | Low | ✅ Implemented |
| **Filtering & Export Options** | Filter photos by likes, ratings, comments, or favorites. Search by filename. Sort by date, name, size, or rating. Export filtered selections as ZIP or generate Capture One/Lightroom-compatible file lists for professional workflows | Medium | ✅ Implemented | | **Filtering & Export Options** | Filter photos by likes, ratings, comments, or favorites. Search by filename. Sort by date, name, size, or rating. Export filtered selections as ZIP or generate Capture One/Lightroom-compatible file lists for professional workflows | Medium | ✅ Implemented |
**Status Legend:** ✅ Implemented | 🚧 In Progress | 🔄 Open | 📋 Planned **Status Legend:** ✅ Implemented | 🚧 In Progress | 🔄 Open | 📋 Planned
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "picpeak-backend", "name": "picpeak-backend",
"version": "2.2.0", "version": "2.2.1",
"description": "Backend for PicPeak event photo sharing platform", "description": "Backend for PicPeak event photo sharing platform",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
+9 -5
View File
@@ -326,8 +326,12 @@ router.post('/logo', adminAuth, requirePermission('settings.edit'), upload.singl
.first(); .first();
if (oldLogoSetting && oldLogoSetting.setting_value) { if (oldLogoSetting && oldLogoSetting.setting_value) {
const oldPath = JSON.parse(oldLogoSetting.setting_value);
try { try {
// Handle both JSON-serialized and legacy raw path values
let oldPath = oldLogoSetting.setting_value;
if (oldPath.startsWith('"')) {
oldPath = JSON.parse(oldPath);
}
await fs.unlink(oldPath); await fs.unlink(oldPath);
} catch (error) { } catch (error) {
console.error('Failed to delete old logo:', error); console.error('Failed to delete old logo:', error);
@@ -355,13 +359,13 @@ router.post('/logo', adminAuth, requirePermission('settings.edit'), upload.singl
await db('app_settings') await db('app_settings')
.insert({ .insert({
setting_key: 'branding_logo_url', setting_key: 'branding_logo_url',
setting_value: publicPath, setting_value: JSON.stringify(publicPath),
setting_type: 'branding', setting_type: 'branding',
updated_at: new Date() updated_at: new Date()
}) })
.onConflict('setting_key') .onConflict('setting_key')
.merge({ .merge({
setting_value: publicPath, setting_value: JSON.stringify(publicPath),
updated_at: new Date() updated_at: new Date()
}); });
@@ -891,13 +895,13 @@ router.post('/favicon', adminAuth, requirePermission('settings.edit'), faviconUp
await db('app_settings') await db('app_settings')
.insert({ .insert({
setting_key: 'branding_favicon_url', setting_key: 'branding_favicon_url',
setting_value: faviconUrl, setting_value: JSON.stringify(faviconUrl),
setting_type: 'branding', setting_type: 'branding',
updated_at: new Date() updated_at: new Date()
}) })
.onConflict('setting_key') .onConflict('setting_key')
.merge({ .merge({
setting_value: faviconUrl, setting_value: JSON.stringify(faviconUrl),
updated_at: new Date() updated_at: new Date()
}); });
+15 -1
View File
@@ -45,6 +45,20 @@ function transformRole(role) {
}; };
} }
/**
* Transform invitation object from snake_case (DB) to camelCase (API)
*/
function transformInvitation(invitation) {
return {
id: invitation.id,
email: invitation.email,
expiresAt: invitation.expires_at,
createdAt: invitation.created_at,
roleName: invitation.role_name,
invitedBy: invitation.invited_by
};
}
/** /**
* GET /me/permissions * GET /me/permissions
* Get current user's permissions * Get current user's permissions
@@ -81,7 +95,7 @@ router.get('/roles', adminAuth, requirePermission('users.view'), handleAsync(asy
*/ */
router.get('/invitations', adminAuth, requirePermission('users.view'), handleAsync(async (req, res) => { router.get('/invitations', adminAuth, requirePermission('users.view'), handleAsync(async (req, res) => {
const invitations = await userManagementService.getPendingInvitations(); const invitations = await userManagementService.getPendingInvitations();
res.json({ invitations }); res.json({ invitations: invitations.map(transformInvitation) });
})); }));
/** /**
+1
View File
@@ -58,6 +58,7 @@ router.get('/', async (req, res) => {
branding_logo_display_header: settingsObject.branding_logo_display_header !== false, branding_logo_display_header: settingsObject.branding_logo_display_header !== false,
branding_logo_display_hero: settingsObject.branding_logo_display_hero !== false, branding_logo_display_hero: settingsObject.branding_logo_display_hero !== false,
branding_logo_display_mode: settingsObject.branding_logo_display_mode || 'logo_and_text', branding_logo_display_mode: settingsObject.branding_logo_display_mode || 'logo_and_text',
branding_hide_powered_by: settingsObject.branding_hide_powered_by === true,
theme_config: settingsObject.theme_config || null, theme_config: settingsObject.theme_config || null,
default_language: settingsObject.general_default_language || 'en', default_language: settingsObject.general_default_language || 'en',
enable_analytics: settingsObject.general_enable_analytics !== false, enable_analytics: settingsObject.general_enable_analytics !== false,
+2 -1
View File
@@ -88,7 +88,8 @@ server {
} }
# Uploads serving proxy (logos, favicons, watermarks) # Uploads serving proxy (logos, favicons, watermarks)
location /uploads { # ^~ modifier stops regex matching, ensuring uploads are proxied not served locally
location ^~ /uploads {
proxy_pass http://backend:3001; proxy_pass http://backend:3001;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $host; proxy_set_header Host $host;
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "picpeak-frontend", "name": "picpeak-frontend",
"private": true, "private": true,
"version": "2.2.0", "version": "2.2.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+9 -8
View File
@@ -81,9 +81,8 @@ export const BrandingPage: React.FC = () => {
useEffect(() => { useEffect(() => {
if (settings) { if (settings) {
const formatted = settingsService.formatBrandingSettings(settings); const formatted = settingsService.formatBrandingSettings(settings);
// Don't set logo_url here - it will be synced from theme // Include logo_url from branding settings
const { logo_url, ...brandingWithoutLogo } = formatted; setBrandingSettings(prev => ({ ...prev, ...formatted }));
setBrandingSettings(prev => ({ ...prev, ...brandingWithoutLogo }));
} }
}, [settings]); }, [settings]);
@@ -91,15 +90,17 @@ export const BrandingPage: React.FC = () => {
useEffect(() => { useEffect(() => {
if (themeSettings) { if (themeSettings) {
const formatted = settingsService.formatThemeSettings(themeSettings) as ThemeConfig; const formatted = settingsService.formatThemeSettings(themeSettings) as ThemeConfig;
if (formatted && Object.keys(formatted).length > 0) { if (formatted && Object.keys(formatted).length > 0) {
// Use the theme's logo URL as stored in the theme config // Use the theme's logo URL as stored in the theme config
setCurrentTheme(formatted); setCurrentTheme(formatted);
setTheme(formatted); setTheme(formatted);
// Always sync the logo URL from theme to branding settings - theme is source of truth // Only sync logo URL from theme if it exists there (logo is stored in branding settings)
setBrandingSettings(prev => ({ ...prev, logo_url: formatted.logoUrl || '' })); if (formatted.logoUrl) {
setBrandingSettings(prev => ({ ...prev, logo_url: formatted.logoUrl }));
}
// Try to identify which preset this matches // Try to identify which preset this matches
for (const [key, preset] of Object.entries(GALLERY_THEME_PRESETS)) { for (const [key, preset] of Object.entries(GALLERY_THEME_PRESETS)) {
if (JSON.stringify(preset.config) === JSON.stringify(formatted)) { if (JSON.stringify(preset.config) === JSON.stringify(formatted)) {
+4
View File
@@ -36,6 +36,10 @@ const config: VitestUserConfig = {
target: 'http://localhost:7101', target: 'http://localhost:7101',
changeOrigin: true, changeOrigin: true,
}, },
'/uploads': {
target: 'http://localhost:7101',
changeOrigin: true,
},
}, },
} }
} }