Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29dc2a3cf1 | |||
| d2cba449b0 | |||
| e0bd19a74d | |||
| 0ab8cbde7f | |||
| 3a8d53f492 | |||
| 804a964ba0 | |||
| d2cd1aa933 | |||
| d7ecf83d32 | |||
| ebb2ce6065 | |||
| 1931d73b60 | |||
| 0d5ce48dcc | |||
| 4872ef71f8 | |||
| b83f4272b5 | |||
| 5df64992c4 | |||
| 7e5e004270 | |||
| 09ce2b80d0 | |||
| 476fcce13f | |||
| c030e87213 | |||
| e6dd89e969 | |||
| e85a68a386 | |||
| 0acce6ab08 |
@@ -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.1.1"
|
||||
".": "2.2.2"
|
||||
}
|
||||
|
||||
@@ -5,6 +5,38 @@ 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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **i18n:** add translations for settings tabs ([c030e87](https://github.com/the-luap/picpeak/commit/c030e872135b39701ef1f4bbb2f28bcaf4ce7fae))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add settings translations and fix manual backup process ([#82](https://github.com/the-luap/picpeak/issues/82)) ([476fcce](https://github.com/the-luap/picpeak/commit/476fcce13f30f9f2d2f98a0c87c25fba09e9eebc))
|
||||
* **backup:** allow manual backups when automated backups are disabled ([e6dd89e](https://github.com/the-luap/picpeak/commit/e6dd89e969fb7018633159155975bd2bd2fb0409))
|
||||
* **db:** improve PostgreSQL connection check in wait-for-db.sh ([e85a68a](https://github.com/the-luap/picpeak/commit/e85a68a386c72c276b4958599b5246e60dfac716))
|
||||
|
||||
## [2.1.1](https://github.com/the-luap/picpeak/compare/v2.1.0...v2.1.1) (2026-01-07)
|
||||
|
||||
|
||||
|
||||
@@ -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.0",
|
||||
"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,
|
||||
|
||||
@@ -710,7 +710,7 @@ async function saveManifestToS3(manifest, manifestFileName, config, result) {
|
||||
return manifestPath;
|
||||
}
|
||||
|
||||
async function runBackupInternal() {
|
||||
async function runBackupInternal(isManual = false) {
|
||||
if (isRunning) {
|
||||
logger.warn('Backup already running, skipping');
|
||||
return;
|
||||
@@ -722,16 +722,25 @@ async function runBackupInternal() {
|
||||
|
||||
try {
|
||||
const config = await resolveConfigWithFallback();
|
||||
if (!config || !normalizeBoolean(config.backup_enabled)) {
|
||||
logger.info('Backup is disabled, skipping');
|
||||
|
||||
// For scheduled backups, check if backup is enabled
|
||||
// Manual backups should always be allowed (just need valid destination config)
|
||||
if (!isManual && (!config || !normalizeBoolean(config.backup_enabled))) {
|
||||
logger.info('Scheduled backup is disabled, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
// For manual backups, just ensure we have a destination configured
|
||||
if (!config || !config.backup_destination_type) {
|
||||
logger.warn('Backup destination not configured');
|
||||
throw new Error('Backup destination not configured. Please configure backup settings first.');
|
||||
}
|
||||
|
||||
const schemaVersion = await getCurrentSchemaVersion();
|
||||
const insertResult = await db('backup_runs').insert({
|
||||
started_at: startTime,
|
||||
status: 'running',
|
||||
backup_type: 'scheduled',
|
||||
backup_type: isManual ? 'manual' : 'scheduled',
|
||||
app_version: packageJson.version,
|
||||
node_version: process.version,
|
||||
db_schema_version: schemaVersion
|
||||
@@ -928,7 +937,7 @@ function stopBackupService() {
|
||||
|
||||
async function triggerManualBackup() {
|
||||
logger.info('Starting manual backup');
|
||||
await service.runBackup();
|
||||
await service.runBackup(true); // Pass flag to indicate manual backup
|
||||
}
|
||||
|
||||
async function getBackupStatus(limit = 10) {
|
||||
|
||||
+35
-17
@@ -7,7 +7,8 @@ host="${DB_HOST:-postgres}"
|
||||
port="${DB_PORT:-5432}"
|
||||
user="${DB_USER:-picpeak}"
|
||||
target_db="${DB_NAME:-picpeak}"
|
||||
default_db="${DB_CHECK_DB:-postgres}"
|
||||
# Use target database for checks - the picpeak user may not have access to 'postgres' database
|
||||
default_db="${DB_CHECK_DB:-$target_db}"
|
||||
|
||||
sanitize_identifier() {
|
||||
printf '%s' "$1" | sed "s/'/''/g"
|
||||
@@ -15,27 +16,44 @@ sanitize_identifier() {
|
||||
|
||||
echo "Waiting for PostgreSQL at $host:$port..."
|
||||
|
||||
# Wait for PostgreSQL server to accept connections (using the default database)
|
||||
until PGPASSWORD="$DB_PASSWORD" psql -h "$host" -p "$port" -U "$user" -d "$default_db" -c '\q' >/dev/null 2>&1; do
|
||||
>&2 echo "PostgreSQL is unavailable - sleeping"
|
||||
# First, wait for PostgreSQL server to be reachable
|
||||
max_attempts=30
|
||||
attempt=0
|
||||
while [ $attempt -lt $max_attempts ]; do
|
||||
if PGPASSWORD="$DB_PASSWORD" psql -h "$host" -p "$port" -U "$user" -d "$target_db" -c '\q' >/dev/null 2>&1; then
|
||||
>&2 echo "PostgreSQL is up - database \"$target_db\" is accessible."
|
||||
break
|
||||
fi
|
||||
|
||||
# If target DB doesn't work, try connecting to 'postgres' or 'template1' to create it
|
||||
if PGPASSWORD="$DB_PASSWORD" psql -h "$host" -p "$port" -U "$user" -d "template1" -c '\q' >/dev/null 2>&1; then
|
||||
>&2 echo "PostgreSQL is up - checking if database \"$target_db\" needs to be created..."
|
||||
|
||||
# Check if database exists
|
||||
db_exists=$(PGPASSWORD="$DB_PASSWORD" psql -h "$host" -p "$port" -U "$user" -d "template1" -tAc "SELECT 1 FROM pg_database WHERE datname = '$(sanitize_identifier "$target_db")'" 2>/dev/null || echo 0)
|
||||
|
||||
if [ "$db_exists" != "1" ]; then
|
||||
>&2 echo "Database \"$target_db\" not found. Attempting to create..."
|
||||
if PGPASSWORD="$DB_PASSWORD" psql -h "$host" -p "$port" -U "$user" -d "template1" -c "CREATE DATABASE \"$target_db\";" >/dev/null 2>&1; then
|
||||
>&2 echo "Database \"$target_db\" created successfully."
|
||||
else
|
||||
>&2 echo "Warning: Could not create database. It may already exist or user lacks permissions."
|
||||
fi
|
||||
fi
|
||||
break
|
||||
fi
|
||||
|
||||
attempt=$((attempt + 1))
|
||||
>&2 echo "PostgreSQL is unavailable - sleeping (attempt $attempt/$max_attempts)"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
>&2 echo "PostgreSQL is up - verifying target database \"$target_db\""
|
||||
|
||||
# Ensure the target database exists (helps when volumes are reused or DB_NAME is customised)
|
||||
db_exists=$(PGPASSWORD="$DB_PASSWORD" psql -h "$host" -p "$port" -U "$user" -d "$default_db" -tAc "SELECT 1 FROM pg_database WHERE datname = '$(sanitize_identifier "$target_db")'" 2>/dev/null || echo 0)
|
||||
|
||||
if [ "$db_exists" != "1" ]; then
|
||||
>&2 echo "Database \"$target_db\" not found. Attempting to create..."
|
||||
if ! PGPASSWORD="$DB_PASSWORD" psql -h "$host" -p "$port" -U "$user" -d "$default_db" -c "CREATE DATABASE \"$target_db\";" >/dev/null 2>&1; then
|
||||
>&2 echo "Failed to create database \"$target_db\". Please ensure it exists and is accessible."
|
||||
exit 1
|
||||
fi
|
||||
>&2 echo "Database \"$target_db\" created successfully."
|
||||
if [ $attempt -eq $max_attempts ]; then
|
||||
>&2 echo "Failed to connect to PostgreSQL after $max_attempts attempts."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Wait until the target database itself is ready to accept connections
|
||||
# Final verification - wait for target database to accept connections
|
||||
until PGPASSWORD="$DB_PASSWORD" psql -h "$host" -p "$port" -U "$user" -d "$target_db" -c '\q' >/dev/null 2>&1; do
|
||||
>&2 echo "Waiting for database \"$target_db\" to accept connections..."
|
||||
sleep 2
|
||||
|
||||
+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.0",
|
||||
"version": "2.2.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -737,6 +737,80 @@
|
||||
"failed": "Failed",
|
||||
"lastUpdate": "Last update"
|
||||
},
|
||||
"events": {
|
||||
"title": "Event Creation",
|
||||
"requiredFields": "Required Fields",
|
||||
"requiredFieldsDescription": "Configure which contact fields are required when creating new events.",
|
||||
"requireCustomerName": "Require customer name",
|
||||
"requireCustomerNameHelp": "Customer name must be provided for new events",
|
||||
"requireCustomerEmail": "Require customer email",
|
||||
"requireCustomerEmailHelp": "Customer email must be provided for new events",
|
||||
"customerEmailWarning": "Required for sending gallery invitations",
|
||||
"requireAdminEmail": "Require admin email",
|
||||
"requireAdminEmailHelp": "Admin email must be provided for new events",
|
||||
"adminEmailWarning": "Required for receiving event notifications",
|
||||
"saveSettings": "Save Event Settings",
|
||||
"noteTitle": "Note",
|
||||
"noteText": "These settings only affect new event creation. Existing events are not affected. Default behavior requires all fields."
|
||||
},
|
||||
"imageSecurity": {
|
||||
"title": "Image Protection",
|
||||
"saveSuccess": "Image security settings saved",
|
||||
"saveError": "Failed to save settings",
|
||||
"loadError": "Failed to load image security settings",
|
||||
"defaultProtection": "Default Protection Settings",
|
||||
"defaultProtectionHelp": "These settings apply to all new events. Individual events can override these defaults.",
|
||||
"protectionLevel": "Default Protection Level",
|
||||
"imageQuality": "Default Image Quality",
|
||||
"fragmentationLevel": "Fragmentation Level",
|
||||
"enableDevtools": "Enable DevTools detection by default",
|
||||
"enableCanvas": "Enable canvas rendering by default (advanced protection)",
|
||||
"rateLimiting": "Rate Limiting",
|
||||
"rateLimitingHelp": "Limit how many images can be requested to prevent scraping.",
|
||||
"requestsPerMinute": "Requests per minute",
|
||||
"requestsPer5Minutes": "Requests per 5 min",
|
||||
"requestsPerHour": "Requests per hour",
|
||||
"securityMonitoring": "Security Monitoring",
|
||||
"suspiciousThreshold": "Suspicious activity threshold",
|
||||
"autoBlockThreshold": "Auto-block threshold",
|
||||
"enableMonitoring": "Enable security monitoring",
|
||||
"blockSuspiciousIps": "Automatically block suspicious IPs",
|
||||
"logEvents": "Log security events to database",
|
||||
"infoTitle": "About Image Protection",
|
||||
"infoText": "These protection features help prevent casual downloading and copying but cannot block all methods. Determined users may still find ways to capture images. Consider using watermarks and legal agreements for comprehensive protection."
|
||||
},
|
||||
"moderation": {
|
||||
"title": "Moderation",
|
||||
"wordFilters": "Word Filters",
|
||||
"description": "Manage words that should be filtered or blocked in comments",
|
||||
"addFilter": "Add New Filter",
|
||||
"enterWord": "Enter word to filter",
|
||||
"searchFilters": "Search filters...",
|
||||
"filterAdded": "Word filter added successfully",
|
||||
"filterExists": "This word filter already exists",
|
||||
"addError": "Failed to add word filter",
|
||||
"filterUpdated": "Word filter updated successfully",
|
||||
"updateError": "Failed to update word filter",
|
||||
"filterDeleted": "Word filter deleted successfully",
|
||||
"deleteError": "Failed to delete word filter",
|
||||
"wordRequired": "Please enter a word to filter",
|
||||
"confirmDelete": "Are you sure you want to delete this word filter?",
|
||||
"loading": "Loading word filters...",
|
||||
"noMatchingFilters": "No matching filters found",
|
||||
"noFilters": "No word filters configured yet",
|
||||
"severityLow": "Low",
|
||||
"severityModerate": "Moderate",
|
||||
"severityHigh": "High",
|
||||
"severityBlock": "Block",
|
||||
"severityLevels": "Severity Levels",
|
||||
"lowDescription": "Word is flagged for review but not automatically blocked",
|
||||
"moderateDescription": "Comment requires manual approval before being visible",
|
||||
"highDescription": "Comment is automatically hidden and requires admin review",
|
||||
"blockDescription": "Comment is rejected immediately and cannot be submitted"
|
||||
},
|
||||
"styling": {
|
||||
"title": "Custom CSS"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytics",
|
||||
"umamiIntegration": "Umami Analytics Integration",
|
||||
@@ -1731,6 +1805,27 @@
|
||||
"testEmailFailed": "Connection test failed"
|
||||
}
|
||||
},
|
||||
"cssTemplates": {
|
||||
"title": "Custom CSS Templates",
|
||||
"template": "Template",
|
||||
"templateName": "Template Name",
|
||||
"enableTemplate": "Enable this template",
|
||||
"enableHint": "Enabled templates can be selected when creating events",
|
||||
"cssContent": "CSS Content",
|
||||
"cssHint": "Use .gallery-page to scope styles to the gallery. Available variables: --gallery-bg, --gallery-text, --gallery-accent",
|
||||
"securityNotice": "Security Notice",
|
||||
"securityText": "CSS is sanitized to prevent malicious code. External URLs, @import, and JavaScript expressions are blocked.",
|
||||
"resetToDefault": "Reset to Default",
|
||||
"resetConfirm": "Reset this template to the default? Your changes will be lost.",
|
||||
"unsavedChanges": "Unsaved changes",
|
||||
"saveTemplate": "Save Template",
|
||||
"lastUpdated": "Last updated",
|
||||
"saved": "Template saved successfully",
|
||||
"saveFailed": "Failed to save template",
|
||||
"sanitizationWarning": "Some CSS patterns were blocked for security",
|
||||
"reset": "Template reset to default",
|
||||
"resetFailed": "Failed to reset template"
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "System Maintenance",
|
||||
"message": "We're currently performing scheduled maintenance to improve our service. We'll be back online shortly.",
|
||||
|
||||
@@ -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