diff --git a/PROJECT.md b/PROJECT.md index 1e009a8..7095f83 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -709,7 +709,7 @@ CREATE TABLE config ( INSERT INTO config (key, value) VALUES ('max_image_size_mb', '20'), ('max_video_size_mb', '500'), - ('upload_rate_per_hour', '10'), + ('upload_rate_per_hour', '100'), -- raised from 10 in migration 015 (guests upload bursts of 10-20) ('feed_rate_per_min', '60'), ('export_rate_per_day', '3'), ('quota_tolerance', '0.75'), diff --git a/backend/migrations/015_raise_upload_rate.down.sql b/backend/migrations/015_raise_upload_rate.down.sql new file mode 100644 index 0000000..17946c8 --- /dev/null +++ b/backend/migrations/015_raise_upload_rate.down.sql @@ -0,0 +1,3 @@ +-- Revert the default upload rate to 10/hour for installs still on the raised +-- default (preserves any explicit admin override at another value). +UPDATE config SET value = '10' WHERE key = 'upload_rate_per_hour' AND value = '100'; diff --git a/backend/migrations/015_raise_upload_rate.up.sql b/backend/migrations/015_raise_upload_rate.up.sql new file mode 100644 index 0000000..677f524 --- /dev/null +++ b/backend/migrations/015_raise_upload_rate.up.sql @@ -0,0 +1,10 @@ +-- Raise the default per-guest upload rate from 10/hour to 100/hour. +-- +-- Rationale: guests routinely upload a burst of 10-20 photos at once (phone +-- multi-select). At the old default of 10/hour a real guest's first burst was +-- throttled — surfaced by the 2026-07-18 load test. 100/hour comfortably covers +-- several bursts across an event while still bounding abuse. +-- +-- Only bump installs still on the old default; an admin who deliberately set a +-- different value keeps it (migration 005 seeded 10; this UPDATE is scoped to '10'). +UPDATE config SET value = '100' WHERE key = 'upload_rate_per_hour' AND value = '10'; diff --git a/backend/src/handlers/test_admin.rs b/backend/src/handlers/test_admin.rs index 917ccbf..7e44c7c 100644 --- a/backend/src/handlers/test_admin.rs +++ b/backend/src/handlers/test_admin.rs @@ -40,13 +40,13 @@ pub async fn truncate_all( .execute(&state.pool) .await?; - // Reseed config — mirrors migrations 005 and 009. Kept in sync by hand + // Reseed config — mirrors migrations 005, 009 and 015. Kept in sync by hand // because pulling SQL out of the migration files at runtime is fragile. sqlx::query( r#"INSERT INTO config (key, value) VALUES ('max_image_size_mb', '20'), ('max_video_size_mb', '500'), - ('upload_rate_per_hour', '10'), + ('upload_rate_per_hour', '100'), ('feed_rate_per_min', '60'), ('export_rate_per_day', '3'), ('quota_tolerance', '0.75'), diff --git a/backend/src/handlers/upload.rs b/backend/src/handlers/upload.rs index aabf8a6..0e84f6a 100644 --- a/backend/src/handlers/upload.rs +++ b/backend/src/handlers/upload.rs @@ -48,7 +48,7 @@ pub async fn upload( let upload_rate_on = config::get_bool(&state.config_cache, "upload_rate_enabled", true).await; if rate_limits_on && upload_rate_on { let upload_rate = - config::get_i64(&state.config_cache, "upload_rate_per_hour", 10).await as usize; + config::get_i64(&state.config_cache, "upload_rate_per_hour", 100).await as usize; if let Err(retry_after_secs) = state.rate_limiter.check_with_retry( format!("upload:{}", auth.user_id), upload_rate, diff --git a/frontend/src/routes/diashow/+page.svelte b/frontend/src/routes/diashow/+page.svelte index 1419d1f..1ed41ff 100644 --- a/frontend/src/routes/diashow/+page.svelte +++ b/frontend/src/routes/diashow/+page.svelte @@ -5,7 +5,7 @@ import { getToken } from '$lib/auth'; import { showBottomNav } from '$lib/ui-store'; import { dataMode, pickMediaUrl } from '$lib/data-mode-store'; - import { onSseEvent } from '$lib/sse'; + import { connectSse, disconnectSse, onSseEvent } from '$lib/sse'; import { SlideQueue } from '$lib/diashow/queue'; import { transitions, findTransition } from '$lib/diashow/transitions'; import { acquireWakeLock, releaseWakeLock } from '$lib/diashow/wakelock'; @@ -233,6 +233,13 @@ unsubs.push(onSseEvent('upload-deleted', handleUploadDeleted)); unsubs.push(onSseEvent('user-hidden', handleUserHidden)); unsubs.push(onSseEvent('feed-delta', handleFeedDelta)); + // Open the stream ourselves — a kiosk/projector loads /diashow directly (never + // via /feed), so we can't rely on another page having opened the singleton + // EventSource. Without this the show only ever gets its one-time /feed snapshot + // and never receives live `upload-processed` events. connectSse() is idempotent + // (no-ops if the feed page already opened it). Subscriptions are registered above + // first so no early event is missed. + connectSse(); void loadInitial(); }); @@ -243,6 +250,7 @@ if (processedDebounce) clearTimeout(processedDebounce); void releaseWakeLock(); for (const unsub of unsubs) unsub(); + disconnectSse(); });