fix: remove the image-fragmentation surface

Closes #1300.

Fragmentation was configurable, stored per event, served to the gallery
client, and consumed by nothing. It was not unbuilt scaffolding — both
halves exist and are individually coherent — but they were never
connected, and they disagree: the server cut a fixed 3x3 grid while the
client reassembled a 4x4 one, so wiring them together as they stood
would have produced scrambled images rather than protection.

Removed rather than finished, because finishing it buys nothing. The
client fetches the whole image and then redraws it in pieces on a
canvas, so the full original has already crossed the wire before any
"protection" is applied — that is obfuscation, not a control. The
per-fragment canvas work also lands on mobile, which is the memory
profile under investigation in #1287.

Goes: secureImageService.fragmentImageBuffer and its branch, the
?fragment=N delivery path and handleFragmentedImage in secureImages,
the fragmented-JSON response in protectedImages, fragmentation_level in
the gallery payload, the default_fragmentation_level setting, the PUT
validator, the ProtectedImage fragment renderer, and the operator
control with its strings in all eight locales.

No migration. `events.fragmentation_level` and the app_settings row stay
— dropping a column is irreversible and the stored values are harmless
once nothing reads them. If they should go, that is a deliberate data
decision and its own migration.

`fragmentGrid` on AuthenticatedImage and the layouts is deliberately
untouched: #1299 already removes it as part of the inert prop surface,
and doing it here would only collide.
This commit is contained in:
Paul Nothaft
2026-09-05 12:11:40 +02:00
parent c71ffae912
commit 967224c030
20 changed files with 9 additions and 281 deletions
@@ -15,7 +15,6 @@ interface ImageSecuritySettings {
max_image_requests_per_hour: number;
suspicious_activity_threshold: number;
enable_canvas_rendering: boolean;
default_fragmentation_level: number;
security_monitoring_enabled: boolean;
block_suspicious_ips: boolean;
log_security_events_to_db: boolean;
@@ -31,7 +30,6 @@ const defaultSettings: ImageSecuritySettings = {
max_image_requests_per_hour: 500,
suspicious_activity_threshold: 10,
enable_canvas_rendering: false,
default_fragmentation_level: 3,
security_monitoring_enabled: true,
block_suspicious_ips: true,
log_security_events_to_db: true,
@@ -161,21 +159,6 @@ export const ImageSecurityTab: React.FC = () => {
/>
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">{t('settings.imageSecurity.imageQualityHelp', '1-100, higher = better quality')}</p>
</div>
<div>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('settings.imageSecurity.fragmentationLevel', 'Fragmentation Level')}
</label>
<input
type="number"
min="1"
max="10"
value={settings.default_fragmentation_level}
onChange={(e) => handleChange('default_fragmentation_level', parseInt(e.target.value) || 3)}
className="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
/>
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">{t('settings.imageSecurity.fragmentationLevelHelp', '1-10, higher = more protection')}</p>
</div>
</div>
<div className="space-y-3 pt-2">