diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 23dc13c0..faa24054 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -45,14 +45,14 @@ jobs: - name: Determine build platforms id: platforms run: | - # For PRs, build only amd64 to avoid QEMU emulation issues with Sharp - # For main/develop/tags, build multi-arch - if [[ "${{ github.event_name }}" == "pull_request" ]]; then - echo "platforms=linux/amd64" >> $GITHUB_OUTPUT - echo "skip_qemu=true" >> $GITHUB_OUTPUT - else + # Only build ARM64 for tagged releases (v*.*.*) + # QEMU emulation is too slow/unreliable for npm operations on regular builds + if [[ "${{ github.ref }}" == refs/tags/v* ]]; then echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT echo "skip_qemu=false" >> $GITHUB_OUTPUT + else + echo "platforms=linux/amd64" >> $GITHUB_OUTPUT + echo "skip_qemu=true" >> $GITHUB_OUTPUT fi - name: Set up QEMU @@ -142,14 +142,14 @@ jobs: - name: Determine build platforms id: platforms run: | - # For PRs, build only amd64 to avoid QEMU emulation issues - # For main/develop/tags, build multi-arch - if [[ "${{ github.event_name }}" == "pull_request" ]]; then - echo "platforms=linux/amd64" >> $GITHUB_OUTPUT - echo "skip_qemu=true" >> $GITHUB_OUTPUT - else + # Only build ARM64 for tagged releases (v*.*.*) + # QEMU emulation is too slow/unreliable for npm operations on regular builds + if [[ "${{ github.ref }}" == refs/tags/v* ]]; then echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT echo "skip_qemu=false" >> $GITHUB_OUTPUT + else + echo "platforms=linux/amd64" >> $GITHUB_OUTPUT + echo "skip_qemu=true" >> $GITHUB_OUTPUT fi - name: Set up QEMU diff --git a/README.md b/README.md index 4e802ae9..30b07bbd 100644 --- a/README.md +++ b/README.md @@ -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 | | **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 | -| **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 | **Status Legend:** ✅ Implemented | 🚧 In Progress | 🔄 Open | 📋 Planned diff --git a/backend/package.json b/backend/package.json index 9cb8ffe3..3f3a7906 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "picpeak-backend", - "version": "2.2.0", + "version": "2.2.1", "description": "Backend for PicPeak event photo sharing platform", "main": "server.js", "scripts": { diff --git a/backend/src/routes/adminSettings.js b/backend/src/routes/adminSettings.js index 35eb5e57..e655e6e3 100644 --- a/backend/src/routes/adminSettings.js +++ b/backend/src/routes/adminSettings.js @@ -326,8 +326,12 @@ router.post('/logo', adminAuth, requirePermission('settings.edit'), upload.singl .first(); if (oldLogoSetting && oldLogoSetting.setting_value) { - const oldPath = JSON.parse(oldLogoSetting.setting_value); 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); } catch (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') .insert({ setting_key: 'branding_logo_url', - setting_value: publicPath, + setting_value: JSON.stringify(publicPath), setting_type: 'branding', updated_at: new Date() }) .onConflict('setting_key') .merge({ - setting_value: publicPath, + setting_value: JSON.stringify(publicPath), updated_at: new Date() }); @@ -891,13 +895,13 @@ router.post('/favicon', adminAuth, requirePermission('settings.edit'), faviconUp await db('app_settings') .insert({ setting_key: 'branding_favicon_url', - setting_value: faviconUrl, + setting_value: JSON.stringify(faviconUrl), setting_type: 'branding', updated_at: new Date() }) .onConflict('setting_key') .merge({ - setting_value: faviconUrl, + setting_value: JSON.stringify(faviconUrl), updated_at: new Date() }); diff --git a/backend/src/routes/adminUsers.js b/backend/src/routes/adminUsers.js index 82d09abc..38d5ded3 100644 --- a/backend/src/routes/adminUsers.js +++ b/backend/src/routes/adminUsers.js @@ -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 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) => { const invitations = await userManagementService.getPendingInvitations(); - res.json({ invitations }); + res.json({ invitations: invitations.map(transformInvitation) }); })); /** diff --git a/backend/src/routes/publicSettings.js b/backend/src/routes/publicSettings.js index 2e41f182..b6e9ac56 100644 --- a/backend/src/routes/publicSettings.js +++ b/backend/src/routes/publicSettings.js @@ -58,6 +58,7 @@ router.get('/', async (req, res) => { branding_logo_display_header: settingsObject.branding_logo_display_header !== false, branding_logo_display_hero: settingsObject.branding_logo_display_hero !== false, 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, default_language: settingsObject.general_default_language || 'en', enable_analytics: settingsObject.general_enable_analytics !== false, diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 2a9b5c54..977eb899 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -88,7 +88,8 @@ server { } # 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_http_version 1.1; proxy_set_header Host $host; diff --git a/frontend/package.json b/frontend/package.json index 137cde8a..647168bc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "picpeak-frontend", "private": true, - "version": "2.2.0", + "version": "2.2.1", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/pages/admin/BrandingPage.tsx b/frontend/src/pages/admin/BrandingPage.tsx index 74fd32cf..4108ac90 100644 --- a/frontend/src/pages/admin/BrandingPage.tsx +++ b/frontend/src/pages/admin/BrandingPage.tsx @@ -81,9 +81,8 @@ export const BrandingPage: React.FC = () => { useEffect(() => { if (settings) { const formatted = settingsService.formatBrandingSettings(settings); - // Don't set logo_url here - it will be synced from theme - const { logo_url, ...brandingWithoutLogo } = formatted; - setBrandingSettings(prev => ({ ...prev, ...brandingWithoutLogo })); + // Include logo_url from branding settings + setBrandingSettings(prev => ({ ...prev, ...formatted })); } }, [settings]); @@ -91,15 +90,17 @@ export const BrandingPage: React.FC = () => { useEffect(() => { if (themeSettings) { const formatted = settingsService.formatThemeSettings(themeSettings) as ThemeConfig; - + if (formatted && Object.keys(formatted).length > 0) { // Use the theme's logo URL as stored in the theme config setCurrentTheme(formatted); setTheme(formatted); - - // Always sync the logo URL from theme to branding settings - theme is source of truth - setBrandingSettings(prev => ({ ...prev, logo_url: formatted.logoUrl || '' })); - + + // Only sync logo URL from theme if it exists there (logo is stored in branding settings) + if (formatted.logoUrl) { + setBrandingSettings(prev => ({ ...prev, logo_url: formatted.logoUrl })); + } + // Try to identify which preset this matches for (const [key, preset] of Object.entries(GALLERY_THEME_PRESETS)) { if (JSON.stringify(preset.config) === JSON.stringify(formatted)) { diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 15c9255b..80aae11a 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -36,6 +36,10 @@ const config: VitestUserConfig = { target: 'http://localhost:7101', changeOrigin: true, }, + '/uploads': { + target: 'http://localhost:7101', + changeOrigin: true, + }, }, } }