Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29dc2a3cf1 | |||
| d2cba449b0 | |||
| e0bd19a74d | |||
| 0ab8cbde7f | |||
| 3a8d53f492 | |||
| 804a964ba0 | |||
| d2cd1aa933 | |||
| d7ecf83d32 | |||
| ebb2ce6065 | |||
| 1931d73b60 | |||
| 0d5ce48dcc | |||
| 4872ef71f8 | |||
| b83f4272b5 | |||
| 5df64992c4 |
@@ -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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "2.2.0"
|
||||
".": "2.2.2"
|
||||
}
|
||||
|
||||
@@ -5,6 +5,24 @@ All notable changes to PicPeak will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2.2.2](https://github.com/the-luap/picpeak/compare/v2.2.1...v2.2.2) (2026-01-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* align backend port to 3000 across all configurations ([3a8d53f](https://github.com/the-luap/picpeak/commit/3a8d53f4927f577c4031c4bc3531e08191dc632a))
|
||||
* Align nginx backend port for production Docker deployments (v2.2.2) ([#88](https://github.com/the-luap/picpeak/issues/88)) ([e0bd19a](https://github.com/the-luap/picpeak/commit/e0bd19a74dd81bdd45be2384820830bd96769e1c))
|
||||
|
||||
## [2.2.1](https://github.com/the-luap/picpeak/compare/v2.2.0...v2.2.1) (2026-01-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* handle legacy non-JSON logo paths when replacing logo ([0d5ce48](https://github.com/the-luap/picpeak/commit/0d5ce48dccf0c61f210725ffae15dafc5e9f7cab))
|
||||
* JSON serialize favicon and logo URLs for PostgreSQL storage ([b83f427](https://github.com/the-luap/picpeak/commit/b83f4272b584f937fea1f47656182e514b12d980))
|
||||
* resolve branding display issues and invitation parsing errors ([1931d73](https://github.com/the-luap/picpeak/commit/1931d73b60d3419203cc8b420841abbfc9e14d2d))
|
||||
* Resolve branding display issues and invitation parsing errors (v2.2.1) ([#86](https://github.com/the-luap/picpeak/issues/86)) ([d7ecf83](https://github.com/the-luap/picpeak/commit/d7ecf83d32ec6608280b96e6cdee48e9a0ad0afa))
|
||||
|
||||
## [2.2.0](https://github.com/the-luap/picpeak/compare/v2.1.1...v2.2.0) (2026-01-08)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "picpeak-backend",
|
||||
"version": "2.1.1",
|
||||
"version": "2.2.2",
|
||||
"description": "Backend for PicPeak event photo sharing platform",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
|
||||
|
||||
@@ -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) });
|
||||
}));
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ services:
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- NODE_ENV=${NODE_ENV:-production}
|
||||
- PORT=3001
|
||||
- PORT=3000
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
|
||||
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com}
|
||||
@@ -43,12 +43,12 @@ services:
|
||||
- ./backup:/backup
|
||||
- ./storage:/app/storage
|
||||
ports:
|
||||
- "${BACKEND_PORT:-3001}:3001"
|
||||
- "${BACKEND_PORT:-3001}:3000"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:3001/health"]
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
+7
-6
@@ -43,7 +43,7 @@ server {
|
||||
|
||||
# API proxy
|
||||
location /api {
|
||||
proxy_pass http://backend:3001;
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
@@ -61,7 +61,7 @@ server {
|
||||
|
||||
# Photo serving proxy
|
||||
location /photos {
|
||||
proxy_pass http://backend:3001;
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -75,7 +75,7 @@ server {
|
||||
|
||||
# Thumbnail serving proxy
|
||||
location /thumbnails {
|
||||
proxy_pass http://backend:3001;
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -88,8 +88,9 @@ server {
|
||||
}
|
||||
|
||||
# Uploads serving proxy (logos, favicons, watermarks)
|
||||
location /uploads {
|
||||
proxy_pass http://backend:3001;
|
||||
# ^~ modifier stops regex matching, ensuring uploads are proxied not served locally
|
||||
location ^~ /uploads {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -103,7 +104,7 @@ server {
|
||||
|
||||
# Delegate root requests to backend for public landing page handling
|
||||
location = / {
|
||||
proxy_pass http://backend:3001/;
|
||||
proxy_pass http://backend:3000/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "picpeak-frontend",
|
||||
"private": true,
|
||||
"version": "2.1.1",
|
||||
"version": "2.2.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -36,6 +36,10 @@ const config: VitestUserConfig = {
|
||||
target: 'http://localhost:7101',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/uploads': {
|
||||
target: 'http://localhost:7101',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user