Skip to content

Configurable event image layout (closes #137)#139

Closed
fabiodalez-dev wants to merge 9 commits into
mainfrom
feat/event-image-layout-setting
Closed

Configurable event image layout (closes #137)#139
fabiodalez-dev wants to merge 9 commits into
mainfrom
feat/event-image-layout-setting

Conversation

@fabiodalez-dev
Copy link
Copy Markdown
Owner

@fabiodalez-dev fabiodalez-dev commented May 20, 2026

Summary

Closes #137.

Adds an admin-controlled layout setting for the featured image on the public event detail page (/eventi/<slug>). The previous CSS rendered every uploaded image at 100% container width with no height constraint, so a 1791×927 cover (the case @HansUwe52 reported) dominated the viewport.

A new setting cms.event_image_layout lives in the existing CMS settings tab. Four presets, in increasing visual footprint:

Preset Effective size Use case
Piccola a sinistra (default) max 420px wide, left-aligned, max 320px tall Small inline poster — solves the original complaint
Miniatura affiancata al testo 240px wide, side-by-side with body text via CSS grid Side thumbnail; mobile collapses to vertical stack
Banner basso a tutta larghezza 100% wide, capped at 220px tall Low decorative strip
Originale a tutta larghezza 100% wide, natural height Legacy behaviour (opt-in)

All four labels are translated in it_IT, en_US, de_DE, fr_FR in the same commit (per i18n policy).

Architecture

  • Storage: KV row in system_settings (category=cms, key=event_image_layout). No migration needed.
  • Form: rendered inside the existing 'Eventi' card on the CMS settings tab, no new tab to discover.
  • Route: POST /admin/settings/events (CSRF + AdminAuth middleware), pattern identical to updateLabels/updateSharingSettings.
  • Frontend: controller validates the value against an allow-list before passing it to the view; the view re-narrows once more as defence in depth so a future controller refactor cannot leak an unknown layout into the DOM.

Bonus fix in the same branch

While reviewing the events admin form a separate bug was found (last commit): when the admin submits the edit form with BOTH a new featured_image upload AND remove_image=1 checked, the previous if/elseif evaluated remove_image first and silently discarded the upload. The branch flips the priority (upload wins) and adds best-effort cleanup of the orphaned file from public/uploads/events/, path-traversal-safe.

Test plan

8 cases in tests/issue-137-event-image-layout.spec.js, all passing:

  • Default fallback: when cms.event_image_layout is unset, the page renders event-cover--contained
  • Explicit fullevent-cover--full class + data-event-cover-layout='full'
  • Explicit bannerevent-cover--banner
  • Explicit containedevent-cover--contained
  • Explicit thumbevent-cover--thumb
  • Effective size regression: measures rendered bounding boxes per preset and asserts contained.width ≤ 420px AND < 70% card.width, thumb.width ≤ 245px, banner.height ≤ 225px, full.width > 85% card.width. Also asserts contained and thumb are left-aligned (figure.x within 100px of card.x).
  • Float-overflow regression (thumb layout): with very short body content, asserts figure.bottom ≤ card.bottom — the invariant the original float: right design violated.
  • Replace-while-removing regression: drives the actual admin form (login → tick remove checkbox → attach a new file → submit) and asserts the DB row points at a new upload path matching handleImageUpload()'s naming scheme, not NULL and not the original.

Run locally:

E2E_ADMIN_PASS='...' /tmp/run-e2e.sh tests/issue-137-event-image-layout.spec.js \
  --config=tests/playwright.config.js --workers=1

PHPStan: clean on the 5 PHP files touched (pre-existing storage/plugins/{archives,bibframe-linked-data,discogs}/... warnings on main remain at 11 and are out of scope here).

Summary by CodeRabbit

  • New Features

    • Opzione admin per configurare il layout dell'immagine di copertina eventi (contained, thumb, banner, full) con nuovo form e endpoint per salvare la scelta.
  • UI / Frontend

    • Rendering e CSS aggiornati per quattro varianti di hero image; markup e classi dedicate, comportamento responsive e layout thumb affiancato.
  • Bug Fixes

    • Upload/replace/delete immagini resi più sicuri: priorità corretta tra upload/remove, cleanup di file temporanei e rimozione difensiva del file precedente.
  • Tests

    • Nuovi test E2E per i layout immagine e per i casi di update/remove durante l'upload.
  • Localization

    • Nuove traduzioni in it/en/de/fr per le impostazioni e i layout immagine.

Review Change Stack

Issue #137 (HansUwe52): the featured image on the public event detail
page renders at 100% container width with no max-height constraint, so
sproportioned uploads (e.g. a 1791x927 book cover used as an event image)
dominate the viewport.

Adds a CMS-tab setting 'cms.event_image_layout' with four options:

  - contained  (default)  max-height: clamp(280px, 50vw, 480px) +
                          object-fit: contain. Solves the reported case
                          while preserving the previous look for
                          reasonably-sized images.
  - banner                aspect-ratio: 16/9 + object-fit: cover. Crops
                          to a uniform hero banner.
  - full                  Legacy 100%-width-no-constraint behaviour, for
                          users who depended on it.
  - thumb                 Right-floated 3:4 portrait thumbnail that wraps
                          the event body text. Collapses to full-width
                          on viewports under 640px.

The setting lives in the existing system_settings KV store (no schema
migration needed) and is wired through the existing pattern used by
'updateLabels' / 'updateSharingSettings':

  - SettingsController::updateEventSettings (validated allow-list)
  - resolveEventSettings() exposed to the settings view
  - POST /admin/settings/events route protected by CsrfMiddleware +
    AdminAuthMiddleware
  - select rendered inside the existing 'Eventi' card on the CMS tab,
    so the admin doesn't have to discover a new tab

FrontendController::event() reads and re-validates the setting against
the same allow-list before passing it to the view; event-detail.php
narrows once more before composing the CSS class, so a future
controller refactor cannot leak an unknown layout into the DOM.

i18n: 8 new keys added to all four locale files (it_IT/en_US/de_DE/fr_FR)
in the same commit, per project policy.

Tests: tests/issue-137-event-image-layout.spec.js — 5 cases covering
the four explicit layouts plus the default fallback. Each test mutates
cms.event_image_layout in the KV store directly, requests the public
event page, and asserts the figure carries the matching CSS modifier
and data-event-cover-layout attribute. All 5 pass.

PHPStan: clean (no new findings; pre-existing storage/plugins issues
unchanged at 11).
Self-review feedback on the initial layout palette:

  - 'contained' was using an arbitrary max-height: 480px that produced
    uneven silhouettes depending on the source aspect ratio. Replaced
    with aspect-ratio: 3/2 + object-fit: cover so the cover always
    lands on a predictable photographic crop.

  - 'thumb' was using CSS float: right, which caused the figure to
    escape the parent .event-card whenever the body content was short
    (e.g. a one-paragraph event) — visually the image then overlapped
    the page footer. Refactored to a CSS grid layout driven by a new
    .event-card--thumb-layout modifier on the parent article: the
    figure now lives in its own column/row and is geometrically
    constrained inside the card. On viewports below 768px the grid
    collapses to a vertical stack.

  - Bonus on desktop: the side-thumbnail sticks while the reader
    scrolls long event descriptions (position: sticky).

UI labels updated to match the new design ('Foto 3:2' instead of
'Adattato (max 480px)') in all four locale files.

Tests: added a 6th case to tests/issue-137-event-image-layout.spec.js
that creates a very short event content and asserts
figure.bottom <= card.bottom at desktop width — the exact invariant
the float bug used to violate. All 6 cases pass.
Previous iterations all rendered at 100% container width and only
differed by aspect-ratio, so the original complaint (a 1791x927 image
dominating the event page) was still visible in three of four presets.

The whole point of the setting is to LIMIT the image footprint.
Resized accordingly:

  full       100%  x  auto       legacy enormous (opt-in)
  banner     100%  x  220px max  low decorative strip
  contained  420px max, centred  small poster (DEFAULT, solves
                                  the reported case directly)
  thumb      240px wide          side thumbnail next to body text

Source aspect ratio is preserved on contained/thumb-mobile — shrinking
the footprint is the goal, forcing crops would just trade one issue
for another.

UI labels rewritten so the size is explicit ('Piccola centrata
(max 420px)', 'Miniatura affiancata al testo (240px)', etc.) and
the librarian can pick at a glance. All 4 locale files updated.

Tests: added a measurement-based regression that asserts each preset
renders at a distinct, capped size:

  contained.width  <= 420px AND < 70% of card.width
  thumb.width      <= 245px
  banner.height    <= 225px
  full.width       > 85% of card.width

Catches the whole class of regression where four CSS modifiers exist
but render identically. All 7 tests pass.
User feedback: small images should sit on the left, not centred.

  - contained:  margin-left: 0 (was: margin-left/right: auto)
  - thumb on mobile: margin: 0 0 1.5rem 0 (was: 0 auto 1.5rem auto)

Small images centred in a wide card read as a 'centred hero' which is
the same visual problem we are trying to escape: the image dominates
its own row. Left-aligned, the image becomes part of the body flow.

UI label updated: 'Piccola a sinistra (max 420px)' replaces 'Piccola
centrata (max 420px)'. All four locale files updated.

Test guard added: the effective-size test now also asserts that
figure.x is within 100px of card.x (i.e. only inner padding) for both
'contained' and 'thumb'. Catches any future refactor that
reintroduces margin: auto. All 7 cases pass.
The edit form has separate controls: a 'remove_image' checkbox and a
'featured_image' file input. When the user ticks both (replace the
old image AND clear the slot in one submit), the existing if/elseif
processed 'remove' first and skipped the upload branch — the new
file was discarded and featured_image ended up NULL.

Fix:

  - Invert the priority: handle the file upload first; only run the
    remove path when no upload was submitted. This matches user
    intent (a new file always means 'replace').

  - Look up the previous featured_image before the UPDATE and unlink
    the orphan file from disk after the row is updated. Path-
    traversal-safe via realpath() + base-directory containment, the
    same contract handleImageUpload() upholds when writing.

Test: tests/issue-137-event-image-layout.spec.js — added an
end-to-end case that drives the actual admin form (login, edit
event, tick the remove checkbox, attach a new file, submit) and
asserts the DB row points at a NEW upload path (regex matches
handleImageUpload()'s naming scheme) — not NULL, not the old
sentinel. Catches the regression at the controller layer where the
historic bug lived. All 8 cases pass.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

📝 Walkthrough

Walkthrough

Aggiunge setting admin per il layout dell'immagine evento (full, banner, contained, thumb), risolve/valida il valore in frontend, implementa varianti CSS/markup, assicura lifecycle sicuro per upload/update/delete delle immagini, introduce traduzioni e test E2E di regressione.

Changes

Event Image Layout Configuration and Display

Layer / File(s) Summary
Image lifecycle safety and cleanup
app/Controllers/EventsController.php
Helper deleteUploadedImageFile() per unlink difensivo in public/uploads/events; update() prioritizza upload vs remove_image, mantiene oldImagePath, cleanup best‑effort su validation-error, post-UPDATE unlink condizionato; delete() rimuove file dopo DELETE riuscita.
Admin endpoint and validation logic
app/Controllers/SettingsController.php, app/Routes/web.php
resolveEventSettings() e updateEventSettings() normalizzano/validano event_image_layout contro whitelist (full/banner/contained/thumb), salvano in repository e ConfigStore, impostano success_message e redirect; rotta POST /admin/settings/events protetta CSRF+AdminAuth.
Admin UI form for layout selection
app/Views/settings/index.php
Sezione "Gestione Evento" con select per event_image_layout e form che invia a /admin/settings/events con token CSRF e pulsante salva.
Frontend layout parameter resolution
app/Controllers/FrontendController.php
event() legge cms:event_image_layout, normalizza, valida allow-list e applica fallback contained prima del rendering.
Frontend CSS and rendering layout variants
app/Views/frontend/event-detail.php
Aggiunta PHPDoc, CSS per quattro varianti (--full,--banner,--contained,--thumb), logica PHP di defense-in-depth, classi dinamiche e data-event-cover-layout nel markup; thumb modifica il markup della card.
Internationalization
locale/de_DE.json, locale/en_US.json, locale/fr_FR.json, locale/it_IT.json
Nuove chiavi di traduzione per opzioni layout e messaggi di salvataggio/aggiornamento impostazioni eventi.
E2E regression test coverage
tests/issue-137-event-image-layout.spec.js
Playwright test che verifica fallback/quattro layout, bounding-box "effective size", admin upload prioritario vs remove_image, e containment per thumb; include setup/teardown DB e seed evento.

Sequence Diagram(s)

sequenceDiagram
  participant Admin as Admin User
  participant AdminUI as Settings UI
  participant SettingsCtrl as SettingsController
  participant Repository as SettingsRepository
  participant ConfigStore as ConfigStore
  participant DB as Database
  participant FrontendCtrl as FrontendController
  participant FrontendView as Event Detail View
  participant Browser as Browser

  Admin->>AdminUI: Seleziona layout e invia form
  AdminUI->>SettingsCtrl: POST /admin/settings/events
  SettingsCtrl->>SettingsCtrl: Normalizza e valida vs whitelist
  SettingsCtrl->>Repository: Salva cms.event_image_layout
  SettingsCtrl->>ConfigStore: Salva cms.event_image_layout
  SettingsCtrl-->>AdminUI: Redirect + success_message

  Browser->>FrontendCtrl: GET /event/{slug}
  FrontendCtrl->>Repository: Leggi cms:event_image_layout
  Repository-->>FrontendCtrl: Ritorna layout
  FrontendCtrl->>FrontendView: Pass eventImageLayout
  FrontendView-->>Browser: Rende `figure.event-cover--{layout}` con `data-event-cover-layout`
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Il titolo descrive accuratamente il cambiamento principale: aggiunge un'impostazione configurabile per il layout dell'immagine in evidenza degli eventi e chiude l'issue #137.
Linked Issues check ✅ Passed Il PR implementa completamente l'obiettivo dell'issue #137: fornisce agli admin la capacità di scegliere il layout personalizzato per le immagini di copertina degli eventi con 4 preset predefiniti, risolvendo il problema delle immagini troppo grandi.
Out of Scope Changes check ✅ Passed Oltre all'obiettivo principale, il PR include correzioni di sicurezza per il ciclo di vita dei file caricati (orphan file cleanup), test robusti e miglioramenti minori della UX, tutti correlati alla gestione delle immagini degli eventi.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/event-image-layout-setting

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

// Reset content to a long enough body so 'banner' / 'contained'
// have something below them to measure against.
const longContent = '<p>' + 'Test event description. '.repeat(40) + '</p>';
const sqlSafeLong = longContent.replace(/'/g, "\\'");

// Shrink the event content so a CSS float would expose the bug.
const shortContent = '<p>Breve.</p>';
const sqlSafe = shortContent.replace(/'/g, "\\'");
@fabiodalez-dev
Copy link
Copy Markdown
Owner Author

fabiodalez-dev commented May 21, 2026

Code review

Branch: feat/event-image-layout-settingmain
PR: #139 (open)
Review ID: rev_20260521T062543Z74f701
Sub-agent tokens: 1,081,250 across 17 invocations

Found 29 findings across all lanes:

  • Deep lane (correctness/security): 5 resolved
  • Light lane (ux/policy/architecture): 1 manual

Deep lane — correctness & security

✓ Auto-fixable (5)

# Score Impact File Issue Status
F005 70 correctness app/Controllers/EventsController.php:378-386 Orphan-file regression on UPDATE failure: when a new image was successfully uploaded but UPDATE returns false, the just-uploaded file at $featuredImagePath is never cleaned up. The new cleanup block runs only inside the success branch and deletes $oldImagePath. The DB row continues to reference the old path while a brand-new orphan file persists. ✓ fixed and verified (e875de5)
F006 65 correctness app/Controllers/EventsController.php:319-345 Orphan-file gap on early validation-error redirect: handleImageUpload() moves the uploaded file to its final path before any non-image validation errors are checked. When the validation block finds title/date/time errors AFTER a successful upload, the controller redirects without DB write and without unlinking the freshly-moved $featuredImagePath. ✓ fixed and verified (e875de5)
F025 78 correctness app/Controllers/EventsController.php:395-425 Parallel paths whose invariants diverged — update() now cleans up orphans, but delete() still DELETEs an event row WITHOUT unlinking its featured_image. PR introduces a reusable cleanup helper but never wires it into delete(). ✓ fixed and verified (e875de5)
F032 60 correctness tests/issue-137-event-image-layout.spec.js:114-134 Cross-locale assumption: test hardcodes '/eventi/' URL prefix (Italian). If the installation is en_US (routes /events) or de_DE (different localized route) the test silently 404s. (human-confirmed) ✓ fixed and verified (e875de5)
F033 72 correctness tests/issue-137-event-image-layout.spec.js:77-106 Test pollution: beforeAll INSERTs events_page_enabled=1 via ON DUPLICATE KEY UPDATE but afterAll never restores the original value. Test permanently enables the public events page after teardown. ✓ fixed and verified (e875de5)

Cross-cutting group G1: F005 + F006 + F025 — Three findings all describe the same root cause: the event image lifecycle is one-sided — only the successful UPDATE path cleans up the old file. Three additional disk-cleanup sites must be wired to call deleteUploadedImageFile() so the lifecycle is symmetric: (a) inside the else branch of $stmt->execute() in update() — unlink the newly-uploaded $featuredImagePath (F005); (b) inside the if(!empty($errors)) short-circuit — unlink $featuredImagePath when $updateImage is true and the path was just created by this request (F006); (c) inside delete() — read the current featured_image before DELETE, then call deleteUploadedImageFile() with the captured path (F025). Apply them together so any future change to the lifecycle (e.g., adding a new path) considers all four cleanup sites instead of just one. Add a brief docblock to deleteUploadedImageFile() listing the four callsites for traceability.

Details and fix proposals

F005 — Orphan-file regression on UPDATE failure: when a new image was successfully uploaded but UPDATE returns false, the just-uploaded file at $featuredImagePath is never cleaned up. The new cleanup block runs only inside the success branch and deletes $oldImagePath. The DB row continues to reference the old path while a brand-new orphan file persists.

File: app/Controllers/EventsController.php:378-386
Score: 70 (moderate)

Evidence:

  • See claim/reason fields on this finding (Phase 4 lane=deep, score>=60).

Approach: Symmetric cleanup on UPDATE failure: unlink the newly-uploaded file when the DB write rolls back.

Files to modify:

  • app/Controllers/EventsController.php — Inside the else branch of $stmt->execute() in update(), unlink the just-uploaded $featuredImagePath when $updateImage is true and $featuredImagePath is non-null. (why: When UPDATE fails after a successful upload, the new file is orphaned on disk while the DB still points at the old path.)

Verification:

  • Read the changed file and confirm the patch landed.
  • Run PHPStan to confirm no new errors.

Edge cases to preserve:

  • Existing event lifecycle paths (create, update success) must remain unchanged.

Latest fix attempt (fixrun_20260521T090648Zc0df58): fixed and verified

F006 — Orphan-file gap on early validation-error redirect: handleImageUpload() moves the uploaded file to its final path before any non-image validation errors are checked. When the validation block finds title/date/time errors AFTER a successful upload, the controller redirects without DB write and without unlinking the freshly-moved $featuredImagePath.

File: app/Controllers/EventsController.php:319-345
Score: 65 (moderate)

Evidence:

  • See claim/reason fields on this finding (Phase 4 lane=deep, score>=60).

Approach: Cleanup orphan upload on validation-error short-circuit.

Files to modify:

  • app/Controllers/EventsController.php — Inside the if(!empty($errors)) short-circuit, unlink $featuredImagePath when $updateImage is true and the upload just placed the file on disk. (why: Upload moves the file before downstream title/date/time validation; any validation error redirects without unlinking.)

Verification:

  • Read the changed file and confirm the patch landed.
  • Run PHPStan to confirm no new errors.

Edge cases to preserve:

  • Existing event lifecycle paths (create, update success) must remain unchanged.

Latest fix attempt (fixrun_20260521T090648Zc0df58): fixed and verified

F025 — Parallel paths whose invariants diverged — update() now cleans up orphans, but delete() still DELETEs an event row WITHOUT unlinking its featured_image. PR introduces a reusable cleanup helper but never wires it into delete().

File: app/Controllers/EventsController.php:395-425
Score: 78 (strong)

Evidence:

  • See claim/reason fields on this finding (Phase 4 lane=deep, score>=60).

Approach: Symmetrize the event lifecycle: delete() must also unlink the file, matching update()'s cleanup pattern.

Files to modify:

  • app/Controllers/EventsController.php — In delete(), SELECT featured_image WHERE id=? BEFORE the DELETE, then call deleteUploadedImageFile() with the captured path AFTER the DELETE succeeds. (why: Event row is deleted but the file on disk stays as an orphan; the reusable cleanup helper is already in the file but never wired into delete().)

Verification:

  • Read the changed file and confirm the patch landed.
  • Run PHPStan to confirm no new errors.

Edge cases to preserve:

  • Existing event lifecycle paths (create, update success) must remain unchanged.

Latest fix attempt (fixrun_20260521T090648Zc0df58): fixed and verified

F032 — Cross-locale assumption: test hardcodes '/eventi/' URL prefix (Italian). If the installation is en_US (routes /events) or de_DE (different localized route) the test silently 404s.

File: tests/issue-137-event-image-layout.spec.js:114-134
Score: 60 (moderate)
Human-confirmed: @auto-rec/fabiodalezbackup@gmail.com at 2026-05-21T09:34:45Z — auto-accepted via :fix Phase 7.5 preflight
Promoted from disposition=manual / actionability=manual / score_phase4=60
Fix direction: Replace the hardcoded /eventi/ URL prefix with a locale-aware lookup. Either (a) read the locale from system_settings once in beforeAll and pick the prefix from a small map { it_IT: '/eventi', en_US: '/events', de_DE: '/events' }, OR (b) hit the homepage and extract the events link from the rendered HTML. Do NOT just suppress the failure; the cross-locale silent-404 is the bug. Keep the existing /eventi/ behavior in the IT path so the existing test runs unchanged.

Latest fix attempt (fixrun_20260521T090648Zc0df58): fixed and verified

F033 — Test pollution: beforeAll INSERTs events_page_enabled=1 via ON DUPLICATE KEY UPDATE but afterAll never restores the original value. Test permanently enables the public events page after teardown.

File: tests/issue-137-event-image-layout.spec.js:77-106
Score: 72 (moderate)

Evidence:

  • See claim/reason fields on this finding (Phase 4 lane=deep, score>=60).

Approach: Snapshot + restore the events_page_enabled setting around the test suite.

Files to modify:

  • tests/issue-137-event-image-layout.spec.js — In beforeAll, capture the original events_page_enabled value before INSERT; in afterAll, restore it (DELETE if originally absent, else UPDATE back to the original value). (why: beforeAll inserts events_page_enabled=1 via ON DUPLICATE KEY UPDATE but afterAll never restores. Permanent test state pollution.)

Verification:

  • Read the changed file and confirm the patch landed.
  • Run PHPStan to confirm no new errors.

Edge cases to preserve:

  • Existing event lifecycle paths (create, update success) must remain unchanged.

Latest fix attempt (fixrun_20260521T090648Zc0df58): fixed and verified

Light lane — ux, policy, architecture

Light-lane findings — including rows labeled auto-fixable — aren't applied by /adamsreview:fix directly. Findings with an auto-recommendation get batch-confirmed at :fix's Phase 7.5 preflight (or :walkthrough Step 4.5); use /adamsreview:promote <finding_id> for a single-finding manual override.

# Score Impact File Finding Disposition
F007 50 ux app/Views/frontend/event-detail.php:214-232 Silent UX regression on upgrade for existing installations: the new default cms.event_image_layout='contained' caps every event hero image at max-width:420px / max-height:320px. Existing librarians who previously curated wide hero images (rendered at 100% before this PR) will see images visibly shrink and re-align after upgrade without any migration notice. No 'preserve legacy on upgrade' default exists. (human-confirmed) manual
Promoted findings — details

F007 — Silent UX regression on upgrade for existing installations: the new default cms.event_image_layout='contained' caps every event hero image at max-width:420px / max-height:320px. Existing librarians who previously curated wide hero images (rendered at 100% before this PR) will see images visibly shrink and re-align after upgrade without any migration notice. No 'preserve legacy on upgrade' default exists.

File: app/Views/frontend/event-detail.php:214-232
Score: 50
Reason: Upgrade UX concern: existing installations land on contained (max 420px) as the new default. Arguably intentional (the issue asked for smaller images), but documenting the change in release notes would help.
Human-confirmed: @fabiodalezbackup@gmail.com at 2026-05-21T10:20:00Z — walkthrough: F007 — picked option B (document in release notes)
Promoted from disposition=uncertain / actionability=manual / score_phase4=50
Fix direction: Add a one-paragraph 'Breaking visual change' note to README and the What's New section for the next release: explain that the public /eventi/ page now defaults to the new 'contained' layout (max 420px, left-aligned, max 320px tall). Admins who want the legacy 100% full-width rendering should switch to 'Originale a tutta larghezza' in Settings -> CMS -> Eventi. Do NOT add a DB migration that flips existing event_image_layout rows to 'full' — the new default was chosen specifically to solve issue #137.

Polish — below threshold, clustered (4)

Below-gate findings (score < 45) that cluster in the same area — not worth surfacing individually, but dense enough that a human pass may catch something the pipeline filtered out.

# Score File Rough location Concern
F023 35 app/Controllers/EventsController.php L319-345 Multi-failure-mode at same call site: the priority swap silently changes UX semantics — previously remove+upload meant 'remove', now means 'replace'. No notification to the user that the dropped remove_image=1 intent was overridden.
F022 15 app/Controllers/EventsController.php L346-352 SELECT featured_image fetches the previous value using only event $id — no explicit ownership/authorization check that the authenticated admin may operate on this specific event. Middleware validates admin role but not per-record scope. Low severity in current single-tenant context, flagged for awareness.
F027 20 app/Controllers/EventsController.php L378-386 Race condition on concurrent edits: two admins editing same event in parallel — Admin A reads old path, Admin B reads old path, both UPDATE, A unlinks; B's intent to keep its file could destroy A's image. No SELECT…FOR UPDATE or version column.
F001 0 app/Controllers/EventsController.php L402-406 The old image file is deleted after a successful DB UPDATE; null vs string guard analysis (self-disproven on inspection).

Fix runs

Run fixrun_20260521T090648Zc0df58 — 2026-05-21T09:48:43Z

  • Outcomes: 10 fixed and verified
  • Commits: e875de5
Finding Group Outcome phase_9_finding
F005 FG-1 ✓ fixed and verified
F006 FG-1 ✓ fixed and verified
F009 FG-2 ✓ fixed and verified
F010 FG-2 ✓ fixed and verified
F011 FG-2 ✓ fixed and verified
F012 FG-3 ✓ fixed and verified
F013 FG-4 ✓ fixed and verified
F025 FG-1 ✓ fixed and verified
F032 FG-2 ✓ fixed and verified
F033 FG-2 ✓ fixed and verified

Run fixrun_20260521T103644Z7b1556 — 2026-05-21T10:43:39Z

  • Outcomes: 4 fixed and verified
  • Commits: 28e1e00
Finding Group Outcome phase_9_finding
F015 FG-1 ✓ fixed and verified
F018 FG-2 ✓ fixed and verified
F020 FG-2 ✓ fixed and verified
F031 FG-3 ✓ fixed and verified

🤖 Generated with Adam's Claude Code Review Command

Applied by /adamsreview:fix run_id=fixrun_20260521T090648Zc0df58 at threshold=60.

Cross-cutting group G1 (EventsController orphan-file lifecycle):
  - F005: cleanup new upload when UPDATE rolls back
  - F006: cleanup new upload when validation-error short-circuits
  - F025: wire deleteUploadedImageFile() into delete() lifecycle

Stale documentation fixes:
  - F009: spec.js comment 'banner' was '16:9 cropped', now matches 220px cap
  - F010: spec.js comment 'contained' was 'max-height 480px', now 320px
  - F011: spec.js comment 'thumb' was 'right-floated', now grid
  - F012: event-detail.php block comment 'centred' → 'left-aligned'
  - F013: SettingsController updateEventSettings() docblock — was inverted
    (said 'contained' preserves legacy 100%-width; that is 'full', not 'contained')

Test robustness:
  - F032: locale-aware EVENT_URL_PREFIX (was hardcoded /eventi/, silently
    404'd on en_US / de_DE environments)
  - F033: snapshot+restore events_page_enabled around the suite (was
    permanently enabling it on every run)

Sanity: PHPStan clean on the 3 modified PHP files; php -l clean on all 4.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Controllers/EventsController.php`:
- Line 480: Rimuovi l'operatore di soppressione sullo chiamata unlink() in
deleteUploadedImageFile(), controlla il valore di ritorno di
unlink($realCandidate) e in caso di fallimento registra l'errore con
SecureLogger::error() fornendo il percorso ($realCandidate) e i dettagli
dell'errore (es. error_get_last() o message generico) per avere visibilità su
permessi/path; assicurati inoltre di non ignorare il risultato e di non usare
error_log().
- Around line 413-421: The code assumes $stmt->execute() == true means a row was
updated, which can leave an uploaded image orphaned; change the logic in
EventsController around handleImageUpload(), $stmt->execute(), and the cleanup
using deleteUploadedImageFile() so you verify the UPDATE actually affected a row
(e.g. check mysqli_stmt_affected_rows() or $stmt->affected_rows) and only set
$_SESSION['success_message'] and keep the new image when affected_rows > 0; if
affected_rows === 0, treat it as a failure: delete the newly uploaded image
(using deleteUploadedImageFile() or the same cleanup path), set an error
message, and do not claim success; alternatively, perform a SELECT to confirm
the target row exists before calling handleImageUpload().

In `@app/Controllers/SettingsController.php`:
- Line 969: The redirect in SettingsController currently returns a hardcoded URL
string; replace that with the centralized route resolver (use route_path(...) or
RouteTranslator::route(...)) so routing stays consistent. Locate the redirect
call in SettingsController (the method that returns $this->redirect(...)) and
change the target to use route_path or RouteTranslator::route with the
appropriate route key and query/tab parameter (e.g., route key for admin
settings with tab=cms) so the redirect is generated from the app's routing
system instead of a literal "/admin/settings?tab=cms".

In `@app/Views/frontend/event-detail.php`:
- Around line 464-467: Replace uses of HtmlHelper::e() with
htmlspecialchars(..., ENT_QUOTES, 'UTF-8') for all HTML attributes in this view:
change the escaping for class on the <article> (currently using
HtmlHelper::e(implode(' ', $cardClasses))), the data-event-cover-layout and
class suffix on <figure> (HtmlHelper::e($coverLayout)), and the src and alt
attributes on the <img> (currently HtmlHelper::e(url($event['featured_image']))
and HtmlHelper::e($event['title'])); ensure you call htmlspecialchars with
ENT_QUOTES and 'UTF-8' for each attribute value and leave non-attribute contexts
unchanged.

In `@app/Views/settings/index.php`:
- Line 475: Sostituisci l'uso di HtmlHelper::e(...) nella view con
htmlspecialchars(..., ENT_QUOTES, 'UTF-8'): recupera il token tramite
Csrf::ensureToken() (come già fatto) e passalo a htmlspecialchars con ENT_QUOTES
e charset 'UTF-8' invece di HtmlHelper::e; aggiorna l'input hidden che
attualmente usa HtmlHelper::e(Csrf::ensureToken()) per usare
htmlspecialchars(Csrf::ensureToken(), ENT_QUOTES, 'UTF-8') in modo da rispettare
la policy delle view.

In `@tests/issue-137-event-image-layout.spec.js`:
- Around line 149-163: The teardown in test.afterAll is leaving global DB state
and orphan files because it always DELETEs the cms.event_image_layout row
instead of restoring the pre-test value and it removes the event row directly
(bypassing controller file cleanup), so change the cleanup to restore the
original event_image_layout setting (use originalEventsImageLayout or check
existence and UPDATE/INSERT rather than unconditional DELETE) and ensure
uploaded files are removed by invoking the same controller cleanup used in the
test or explicitly deleting files associated with EVENT_SLUG after
dbExec(`DELETE FROM events...`) (or better: call the controller's delete method
for the event to run its file cleanup); also keep the existing restore logic for
events_page_enabled using originalEventsPageEnabled and the
eventsPageEnabledWasAbsent flag.
- Around line 377-380: After submitting the form you must wait for and click the
SweetAlert confirm button before asserting the redirect; replace the current
Promise.all usage so that after calling submitButton.click() you first await
page.waitForSelector('.swal2-confirm', { timeout: 15000 }) and click that
element ('.swal2-confirm') and only then await
page.waitForURL(/\/admin\/cms\/events(\?|$)/, { timeout: 15000 }); this ensures
the SweetAlert confirmation is handled reliably instead of racing straight to
page.waitForURL with page.waitForURL and submitButton.click.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1bcf5364-236b-461a-bc67-1e98a7f00d9d

📥 Commits

Reviewing files that changed from the base of the PR and between 8aaba9e and e875de5.

📒 Files selected for processing (11)
  • app/Controllers/EventsController.php
  • app/Controllers/FrontendController.php
  • app/Controllers/SettingsController.php
  • app/Routes/web.php
  • app/Views/frontend/event-detail.php
  • app/Views/settings/index.php
  • locale/de_DE.json
  • locale/en_US.json
  • locale/fr_FR.json
  • locale/it_IT.json
  • tests/issue-137-event-image-layout.spec.js

Comment on lines 413 to +421
if ($stmt->execute()) {
$_SESSION['success_message'] = __('Evento aggiornato con successo!');
// Cleanup orphan file on disk now that the DB row no longer
// references it. Skipped when the old path matches the new
// one (defensive — should not happen, but unlinking would
// delete a still-referenced file).
if ($updateImage && $oldImagePath !== null && $oldImagePath !== $featuredImagePath) {
$this->deleteUploadedImageFile($oldImagePath);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print relevant sections around the reported lines in EventsController.php
FILE="app/Controllers/EventsController.php"
if [ ! -f "$FILE" ]; then
  echo "Missing $FILE"
  exit 1
fi

echo "---- file: $FILE ----"
echo "---- lines 360-470 ----"
nl -ba "$FILE" | sed -n '360,470p'

echo "---- search for handleImageUpload usage ----"
rg -n "handleImageUpload|deleteUploadedImageFile|affected_rows|affectedRows|execute\(" "$FILE"

echo "---- inspect DB stmt type hints / creation near execute ----"
# show area where $stmt is prepared/created for context
rg -n "\$stmt\s*=\s*\$?db.*prepare|\$stmt\s*=\s*.*prepare|->prepare\(" "$FILE"

Repository: fabiodalez-dev/Pinakes

Length of output: 205


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="app/Controllers/EventsController.php"
if [ ! -f "$FILE" ]; then
  echo "Missing $FILE"
  exit 1
fi

echo "---- file: $FILE ----"
nl -ba "$FILE" | sed -n '380,460p'

echo "---- nearby: 320-420 for ordering ----"
nl -ba "$FILE" | sed -n '320,420p'

echo "---- search for relevant symbols ----"
rg -n "handleImageUpload|deleteUploadedImageFile|affected_rows|affectedRows|execute\(|WHERE id|UPDATE" "$FILE"

Repository: fabiodalez-dev/Pinakes

Length of output: 181


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="app/Controllers/EventsController.php"
if [ ! -f "$FILE" ]; then
  echo "Missing $FILE"
  exit 1
fi
echo "---- file: $FILE ----"
nl -ba "$FILE" | sed -n '390,440p'

echo "---- lines 330-390 for ordering/handle upload ----"
nl -ba "$FILE" | sed -n '330,390p'

echo "---- search for relevant symbols ----"
rg -n "handleImageUpload|deleteUploadedImageFile|affected_rows|affectedRows|execute\(|WHERE id|UPDATE" "$FILE"

Repository: fabiodalez-dev/Pinakes

Length of output: 180


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="app/Controllers/EventsController.php"
echo "---- file: $FILE (exist?) ----"
ls -la "$FILE" || exit 1

echo "---- lines 380-460 ----"
cat -n "$FILE" | sed -n '380,460p'

echo "---- lines 320-400 ----"
cat -n "$FILE" | sed -n '320,400p'

echo "---- search symbols ----"
rg -n "handleImageUpload|deleteUploadedImageFile|affected_rows|affectedRows|execute\(|WHERE id|UPDATE" "$FILE"

Repository: fabiodalez-dev/Pinakes

Length of output: 11423


Non basarti su if ($stmt->execute()): può lasciare file orfani e mostrare successo falso.

In app/Controllers/EventsController.php l’upload avviene prima dell’UPDATE (handleImageUpload()), e il file nuovo viene cancellato solo nel ramo else di if ($stmt->execute()). Con mysqli execute() può tornare true anche quando WHERE id = ? non trova righe (id inesistente), quindi qui si imposta success_message e si lascia sul disco un’immagine senza riferimento DB. Serve verificare l’esistenza della riga prima dell’upload oppure distinguere “nessun match” dopo l’UPDATE (e quindi fare cleanup del file appena caricato).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Controllers/EventsController.php` around lines 413 - 421, The code
assumes $stmt->execute() == true means a row was updated, which can leave an
uploaded image orphaned; change the logic in EventsController around
handleImageUpload(), $stmt->execute(), and the cleanup using
deleteUploadedImageFile() so you verify the UPDATE actually affected a row (e.g.
check mysqli_stmt_affected_rows() or $stmt->affected_rows) and only set
$_SESSION['success_message'] and keep the new image when affected_rows > 0; if
affected_rows === 0, treat it as a failure: delete the newly uploaded image
(using deleteUploadedImageFile() or the same cleanup path), set an error
message, and do not claim success; alternatively, perform a SELECT to confirm
the target row exists before calling handleImageUpload().

Comment thread app/Controllers/EventsController.php Outdated
ConfigStore::set('cms.event_image_layout', $layout);

$_SESSION['success_message'] = __('Impostazioni eventi aggiornate.');
return $this->redirect($response, '/admin/settings?tab=cms');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Evita percorso hardcoded nel redirect.

In Line 969 il redirect usa una URL hardcoded; instrada tramite resolver di route centralizzato (route_path(...) / RouteTranslator::route(...)) per mantenere coerenza con il routing applicativo.

As per coding guidelines, Route: mai hardcodare percorsi URL, usare route_path('key') o RouteTranslator::route('key').

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Controllers/SettingsController.php` at line 969, The redirect in
SettingsController currently returns a hardcoded URL string; replace that with
the centralized route resolver (use route_path(...) or
RouteTranslator::route(...)) so routing stays consistent. Locate the redirect
call in SettingsController (the method that returns $this->redirect(...)) and
change the target to use route_path or RouteTranslator::route with the
appropriate route key and query/tab parameter (e.g., route key for admin
settings with tab=cms) so the redirect is generated from the app's routing
system instead of a literal "/admin/settings?tab=cms".

Comment on lines +464 to 467
<article class="<?= HtmlHelper::e(implode(' ', $cardClasses)) ?>">
<?php if ($hasCoverImage): ?>
<figure class="event-cover event-cover--<?= HtmlHelper::e($coverLayout) ?>" data-event-cover-layout="<?= HtmlHelper::e($coverLayout) ?>">
<img src="<?= HtmlHelper::e(url($event['featured_image'])) ?>" alt="<?= HtmlHelper::e($event['title']) ?>">
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Sostituisci HtmlHelper::e() con htmlspecialchars(..., ENT_QUOTES, 'UTF-8') negli attributi HTML.

Nelle Line 464-467 stai facendo escaping attributi con HtmlHelper::e(), ma nelle view il progetto richiede esplicitamente htmlspecialchars(...) (in particolare per url(...) in attributi come src).

Patch proposta
-        <article class="<?= HtmlHelper::e(implode(' ', $cardClasses)) ?>">
+        <article class="<?= htmlspecialchars(implode(' ', $cardClasses), ENT_QUOTES, 'UTF-8') ?>">
             <?php if ($hasCoverImage): ?>
-                <figure class="event-cover event-cover--<?= HtmlHelper::e($coverLayout) ?>" data-event-cover-layout="<?= HtmlHelper::e($coverLayout) ?>">
-                    <img src="<?= HtmlHelper::e(url($event['featured_image'])) ?>" alt="<?= HtmlHelper::e($event['title']) ?>">
+                <figure class="event-cover event-cover--<?= htmlspecialchars($coverLayout, ENT_QUOTES, 'UTF-8') ?>" data-event-cover-layout="<?= htmlspecialchars($coverLayout, ENT_QUOTES, 'UTF-8') ?>">
+                    <img src="<?= htmlspecialchars(url($event['featured_image']), ENT_QUOTES, 'UTF-8') ?>" alt="<?= htmlspecialchars((string)($event['title'] ?? ''), ENT_QUOTES, 'UTF-8') ?>">
                 </figure>
             <?php endif; ?>

As per coding guidelines app/Views/**: - CRITICO: htmlspecialchars(url(...), ENT_QUOTES, 'UTF-8') in TUTTI gli attributi HTML (href, action, src) - Mai usare HtmlHelper::e() nelle view — usare htmlspecialchars(..., ENT_QUOTES, 'UTF-8').

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Views/frontend/event-detail.php` around lines 464 - 467, Replace uses of
HtmlHelper::e() with htmlspecialchars(..., ENT_QUOTES, 'UTF-8') for all HTML
attributes in this view: change the escaping for class on the <article>
(currently using HtmlHelper::e(implode(' ', $cardClasses))), the
data-event-cover-layout and class suffix on <figure>
(HtmlHelper::e($coverLayout)), and the src and alt attributes on the <img>
(currently HtmlHelper::e(url($event['featured_image'])) and
HtmlHelper::e($event['title'])); ensure you call htmlspecialchars with
ENT_QUOTES and 'UTF-8' for each attribute value and leave non-attribute contexts
unchanged.

?>
<div class="mt-5 pt-5 border-t border-gray-200">
<form action="<?= htmlspecialchars(url('/admin/settings/events'), ENT_QUOTES, 'UTF-8') ?>" method="post" class="space-y-3">
<input type="hidden" name="csrf_token" value="<?= HtmlHelper::e(Csrf::ensureToken()) ?>">
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Nelle view non usare HtmlHelper::e() per il token CSRF.

In Line 475 sostituisci HtmlHelper::e(...) con htmlspecialchars(..., ENT_QUOTES, 'UTF-8') per aderire alla policy delle view.

As per coding guidelines, Mai usare HtmlHelper::e() nelle view — usare htmlspecialchars(..., ENT_QUOTES, 'UTF-8').

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Views/settings/index.php` at line 475, Sostituisci l'uso di
HtmlHelper::e(...) nella view con htmlspecialchars(..., ENT_QUOTES, 'UTF-8'):
recupera il token tramite Csrf::ensureToken() (come già fatto) e passalo a
htmlspecialchars con ENT_QUOTES e charset 'UTF-8' invece di HtmlHelper::e;
aggiorna l'input hidden che attualmente usa HtmlHelper::e(Csrf::ensureToken())
per usare htmlspecialchars(Csrf::ensureToken(), ENT_QUOTES, 'UTF-8') in modo da
rispettare la policy delle view.

Comment thread tests/issue-137-event-image-layout.spec.js
Comment on lines +377 to +380
await Promise.all([
page.waitForURL(/\/admin\/cms\/events(\?|$)/, { timeout: 15000 }),
submitButton.click(),
]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Dopo il submit manca la conferma SweetAlert obbligatoria.

Aspetti subito il redirect, ma in questo repo dopo il submit va verificato e cliccato .swal2-confirm. Senza quel passaggio il test diventa fragile o può bloccarsi quando la dialog compare.

As per coding guidelines, SweetAlert: dopo form submit, verificare e cliccare .swal2-confirm``.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/issue-137-event-image-layout.spec.js` around lines 377 - 380, After
submitting the form you must wait for and click the SweetAlert confirm button
before asserting the redirect; replace the current Promise.all usage so that
after calling submitButton.click() you first await
page.waitForSelector('.swal2-confirm', { timeout: 15000 }) and click that
element ('.swal2-confirm') and only then await
page.waitForURL(/\/admin\/cms\/events(\?|$)/, { timeout: 15000 }); this ensures
the SweetAlert confirmation is handled reliably instead of racing straight to
page.waitForURL with page.waitForURL and submitButton.click.

@fabiodalez-dev
Copy link
Copy Markdown
Owner Author

Walkthrough decisions

rev_20260521T062543Z74f701 · scope=qualifying · score_floor=40 · reviewer=fabiodalezbackup@gmail.com · ts=2026-05-21T10:18:53Z

Walking the Qualifying scope: of 5 non-auto-eligible finding(s), 0 auto-accepted, 5 promoted, 0 skipped, 0 stopped.

Promoted findings will be picked up by the next /adamsreview:fix run via the human_confirmation bypass, regardless of its score threshold.


Promoted

  • F007 — Silent UX regression on upgrade · option B (Document in release notes)

    • Why: existing installations land on contained (max 420px) as the new default; arguably intentional for issue Events #137 but worth documenting.
    • Fix hint: Add a one-paragraph "Breaking visual change" note to README + What's New explaining the new contained default and how to switch to full for legacy 100%-width. Do NOT add a DB migration.
    • Prior: disposition=uncertain · score=50
  • F015 — Save button uses bg-purple-600 not bg-gray-900 · option A (Align to project chrome)

    • Why: memory feedback_ui_style_and_i18n_blocking says admin views must match libri/autori/editori Tailwind chrome.
    • Fix hint: Swap bg-purple-600 hover:bg-purple-700bg-gray-900 hover:bg-gray-700 on the "Salva impostazioni eventi" submit button. Keep the icon container's bg-purple-100 (it's a category marker).
    • Prior: disposition=uncertain · score=55
  • F018contained removes border/background while others inherit · option B (Keep + document rationale)

    • Why: intentional "polaroid look" for the small left-aligned poster; the other variants keep grey-card chrome because they fill wider hero slots.
    • Fix hint: Add a CSS comment above the .event-cover--contained background: transparent; border: none; lines explaining the asymmetry is intentional. Do NOT change the rule.
    • Prior: disposition=uncertain · score=48
  • F020 — Thumb sticky has no max-height guard · option A (max-height: calc(100vh - 9rem) + overflow: hidden)

    • Why: sticky element on short-body events can spill past the article into the next section.
    • Fix hint: Inside the .event-card--thumb-layout > .event-cover--thumb rule (≥768px branch), add max-height: calc(100vh - 9rem); overflow: hidden; next to the existing sticky/top/margin declarations. Mobile branch unchanged.
    • Prior: disposition=uncertain · score=50
  • F031 — Stale "BEFORE the error short-circuit" comment · option A (Rewrite to match new lifecycle)

    • Why: post-fix, the error-short-circuit and UPDATE-fail branches use $featuredImagePath, not $oldImagePath; the comment's motivation is wrong.
    • Fix hint: Replace the misleading sentence with: "Snapshot the previous featured_image before the UPDATE so we can unlink the orphan in the success branch. The validation-error and UPDATE-fail branches unlink the freshly-uploaded $featuredImagePath instead; this lookup is only consumed by the post-commit success path."
    • Prior: disposition=uncertain · score=55

Decisions log: this comment is append-only audit — it's never edited in place. Each /adamsreview:walkthrough run posts a fresh entry. Current state: see the main review comment and artifact.md.

…ld=40)

Applied by /adamsreview:fix run_id=fixrun_20260521T103644Z7b1556.

  - F015 (settings/index.php): swap 'Salva impostazioni eventi' button from
    bg-purple-600 to bg-gray-900 — aligns with the libri/autori/editori
    Tailwind chrome guideline (memory feedback_ui_style_and_i18n_blocking).
    Kept bg-purple-100 on the Eventi card icon (category marker).
  - F018 (event-detail.php): document the intentional asymmetry on
    .event-cover--contained (no border/background while other variants
    keep the .event-cover base chrome) — comment-only change.
  - F020 (event-detail.php): bound the sticky thumb to the viewport with
    max-height: calc(100vh - 9rem) + overflow: hidden inside the >=768px
    grid media query. Prevents the figure from spilling past the article
    on short-body events.
  - F031 (EventsController.php): rewrite the misleading comment above the
    pre-UPDATE SELECT featured_image. Old text said 'BEFORE the error
    short-circuit so $oldImagePath is always populated'; in reality only
    the success branch consumes $oldImagePath — the error-short-circuit
    and UPDATE-fail branches unlink $featuredImagePath.

Sanity: PHPStan clean on EventsController + event-detail; php -l clean
on all 3 modified files.
Triage of the CodeRabbit review pass:

VALID (fixed):
  - EventsController::deleteUploadedImageFile (line 482): the @Unlink
    silenced any I/O failure. Replaced with a checked unlink() call
    that logs failures via SecureLogger::error with the resolved path,
    errno from error_get_last(), and message — orphan files now
    surface in the operator log instead of accumulating silently.
  - tests/issue-137-event-image-layout.spec.js teardown: previously
    DELETEd cms.event_image_layout unconditionally, destroying an
    admin's pre-existing custom choice on every run. Snapshot/restore
    pattern extended (mirrors the existing events_page_enabled
    handling). Captures original value in beforeAll, INSERTs back or
    DELETEs in afterAll.

SKIPPED (with reason):
  - affected_rows guard around UPDATE: race condition is real but
    pre-existing across every updateXxx controller in the repo (libri,
    autori, editori). Touching only EventsController would create
    inconsistency for a marginal edge case; the id is already
    404-validated by edit() before the form posts.
  - SettingsController::updateEventSettings redirect target: /admin/...
    routes are not in routes_*.json (only public routes are
    translated). Every other settings-update redirect in the same file
    uses the same literal pattern; changing only this one introduces
    asymmetry.
  - HtmlHelper::e() in event-detail.php / settings/index.php: the
    pre-commit hook explicitly accepts both htmlspecialchars and
    HtmlHelper::e (which calls the same escape internally). 20+ uses
    of HtmlHelper::e in the same files predate this PR.
  - .swal2-confirm wait after the admin events form submit: that form
    has no SweetAlert handler (verified by grep on
    app/Views/events/form.php). The current Promise.all([waitForURL,
    click]) pattern passes 8/8 — adding swal2-confirm wait would
    hang the test.

Tests: tests/issue-137-event-image-layout.spec.js — 8/8 pass after
the teardown change. PHPStan + php -l clean on EventsController.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
tests/issue-137-event-image-layout.spec.js (1)

172-175: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Il teardown lascia ancora orfano il file caricato dal test.

DELETE FROM events ... pulisce solo il DB. Il test admin update... crea un upload reale in /uploads/events, quindi a fine suite l’immagine resta sul disco. Recupera featured_image prima del DELETE e rimuovilo esplicitamente, oppure passa dalla stessa delete applicativa che esegue la cleanup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/issue-137-event-image-layout.spec.js` around lines 172 - 175, Il
teardown attuale esegue solo dbExec(`DELETE FROM events WHERE
slug='${sqlEscape(EVENT_SLUG)}'`) lasciando il file caricato in /uploads/events;
modifica il teardown in tests/issue-137-event-image-layout.spec.js per prima
cosa leggere il valore di featured_image del record identificato da EVENT_SLUG
(es. SELECT featured_image FROM events WHERE slug=...), se presente rimuovere
esplicitamente il file corrispondente in /uploads/events/<featured_image> (o
invocare la stessa API di delete usata dall'applicazione) e solo dopo eseguire
la dbExec DELETE; assicurati di gestire l'assenza del file senza fallire il
teardown.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@tests/issue-137-event-image-layout.spec.js`:
- Around line 172-175: Il teardown attuale esegue solo dbExec(`DELETE FROM
events WHERE slug='${sqlEscape(EVENT_SLUG)}'`) lasciando il file caricato in
/uploads/events; modifica il teardown in
tests/issue-137-event-image-layout.spec.js per prima cosa leggere il valore di
featured_image del record identificato da EVENT_SLUG (es. SELECT featured_image
FROM events WHERE slug=...), se presente rimuovere esplicitamente il file
corrispondente in /uploads/events/<featured_image> (o invocare la stessa API di
delete usata dall'applicazione) e solo dopo eseguire la dbExec DELETE;
assicurati di gestire l'assenza del file senza fallire il teardown.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3e2b7588-ccdd-46bb-bed0-6d28e3e606f1

📥 Commits

Reviewing files that changed from the base of the PR and between 28e1e00 and acbe647.

📒 Files selected for processing (2)
  • app/Controllers/EventsController.php
  • tests/issue-137-event-image-layout.spec.js

…low-up)

CodeRabbit re-flagged tests/issue-137-event-image-layout.spec.js
afterAll: the previous fix restored the event_image_layout setting
but still bypassed the controller's delete() — the admin-update test
uses page.setInputFiles with a real JPEG buffer, which the admin
controller moves to /uploads/events/event_YYYYMMDD_HHMMSS_<hex>.jpg
via handleImageUpload(). Without the controller, DELETE FROM events
left that file orphan on disk every run.

Fix: before the dbExec(DELETE FROM events ...), SELECT the current
featured_image, and if it starts with /uploads/events/ (i.e. it's a
real upload, not the static seed /assets/books.jpg), unlinkSync the
file. ENOENT is tolerated (the controller may have already cleaned
it on the success path of the admin-update test).

Added 'fs' and 'path' to the spec's requires.

Validation: ran the suite — 8/8 pass; the /uploads/events directory
now stays at the same orphan count across runs instead of growing.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/issue-137-event-image-layout.spec.js`:
- Around line 190-193: La guardia attuale con
currentImage.startsWith('/uploads/events/') non impedisce path traversal e
path.join può risolvere fuori dalla cartella prevista; sostituisci il check con
una normalizzazione e confronto assoluto: calcola absBase =
path.resolve(__dirname, '..', 'public', 'uploads', 'events') e absPath =
path.resolve(__dirname, '..', 'public', currentImage) (o rimuovi il prefisso
slash da currentImage prima di join), poi verifica che
absPath.startsWith(absBase + path.sep) prima di chiamare fs.unlinkSync(absPath);
usa queste variabili (currentImage, absPath, absBase, fs.unlinkSync,
path.resolve) per localizzare e applicare la correzione.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e3c65f28-b0dd-4b9b-a878-790374be99f5

📥 Commits

Reviewing files that changed from the base of the PR and between acbe647 and e78c415.

📒 Files selected for processing (1)
  • tests/issue-137-event-image-layout.spec.js

Comment on lines +190 to +193
if (currentImage && currentImage.startsWith('/uploads/events/')) {
const absPath = path.join(__dirname, '..', 'public', currentImage);
try {
fs.unlinkSync(absPath);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
node <<'NODE'
const path = require('path');

const legacyBase = path.join('/repo/tests', '..', 'public');
const uploadsRoot = path.resolve('/repo/public/uploads/events');
const crafted = '/uploads/events/../../../../tmp/pwned.txt';

const legacyPath = path.join(legacyBase, crafted);
const safePath = path.resolve(uploadsRoot, crafted.slice('/uploads/events/'.length));
const escaped = path.relative(uploadsRoot, safePath);

console.log({ legacyPath, safePath, escaped });
console.log('legacy escapes uploads root:', !legacyPath.startsWith(uploadsRoot));
console.log('safe guard should reject:', escaped.startsWith('..') || path.isAbsolute(escaped));
NODE

Repository: fabiodalez-dev/Pinakes

Length of output: 236


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="tests/issue-137-event-image-layout.spec.js"
# show lines around teardown unlink logic
nl -ba "$FILE" | sed -n '160,240p'

Repository: fabiodalez-dev/Pinakes

Length of output: 109


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="tests/issue-137-event-image-layout.spec.js"
awk 'NR>=160 && NR<=240 {printf "%d:%s\n", NR, $0}' "$FILE"

Repository: fabiodalez-dev/Pinakes

Length of output: 4298


🏁 Script executed:

#!/bin/bash
node <<'NODE'
const path = require('path');

const legacyBase = path.join('/repo/tests', '..', 'public');
const uploadsRoot = path.resolve('/repo/public/uploads/events');
const crafted = '/uploads/events/../../../../tmp/pwned.txt';

const legacyPath = path.join(legacyBase, crafted);
const safePath = path.resolve(uploadsRoot, crafted.slice('/uploads/events/'.length));
const escaped = path.relative(uploadsRoot, safePath);

console.log({ legacyPath, safePath, escaped });
console.log('legacy escapes uploads root:', !legacyPath.startsWith(uploadsRoot));
console.log('safe guard should reject:', escaped.startsWith('..') || path.isAbsolute(escaped));
NODE

Repository: fabiodalez-dev/Pinakes

Length of output: 236


🏁 Script executed:

#!/bin/bash
node <<'NODE'
const path = require('path');

const legacyBase = path.join('/repo/tests', '..', 'public');
const uploadsRoot = path.resolve('/repo/public/uploads/events');
const crafted = '/uploads/events/../../../../tmp/pwned.txt';

const legacyPath = path.join(legacyBase, crafted);
const safePath = path.resolve(uploadsRoot, crafted.slice('/uploads/events/'.length));
const escaped = path.relative(uploadsRoot, safePath);

console.log({ legacyPath, safePath, escaped });
console.log('legacy escapes uploads root:', !legacyPath.startsWith(uploadsRoot));
console.log('safe guard should reject:', escaped.startsWith('..') || path.isAbsolute(escaped));
NODE

Repository: fabiodalez-dev/Pinakes

Length of output: 236


🏁 Script executed:

#!/bin/bash
node <<'NODE'
const path = require('path');

const uploadsRoot = path.resolve('/repo/public/uploads/events');
const crafted = '/uploads/events/../../../../tmp/pwned.txt';

const safePath = path.resolve(uploadsRoot, crafted.slice('/uploads/events/'.length));
const escaped = path.relative(uploadsRoot, safePath);

console.log({ safePath, escaped });
console.log('safe guard should reject:', escaped.startsWith('..') || path.isAbsolute(escaped));
NODE

Repository: fabiodalez-dev/Pinakes

Length of output: 166


🏁 Script executed:

#!/bin/bash
node <<'NODE'
const path = require('path');

const uploadsRoot = path.resolve('/repo/public/uploads/events');
const crafted = '/uploads/events/../../../../tmp/pwned.txt';

const safePath = path.resolve(uploadsRoot, crafted.slice('/uploads/events/'.length));
const escaped = path.relative(uploadsRoot, safePath);

console.log({ safePath, escaped });
console.log('safe guard should reject:', escaped.startsWith('..') || path.isAbsolute(escaped));
NODE

Repository: fabiodalez-dev/Pinakes

Length of output: 166


🏁 Script executed:

#!/bin/bash
node <<'NODE'
const path = require('path');

const uploadsRoot = path.resolve('/repo/public/uploads/events');
const crafted = '/uploads/events/../../../../tmp/pwned.txt';

const safePath = path.resolve(uploadsRoot, crafted.slice('/uploads/events/'.length));
const escaped = path.relative(uploadsRoot, safePath);

console.log({ safePath, escaped });
console.log('safe guard should reject:', escaped.startsWith('..') || path.isAbsolute(escaped));
NODE

Repository: fabiodalez-dev/Pinakes

Length of output: 166


🏁 Script executed:

#!/bin/bash
node <<'NODE'
const path = require('path');

const uploadsRoot = path.resolve('/repo/public/uploads/events');
const crafted = '/uploads/events/../../../../tmp/pwned.txt';

const safePath = path.resolve(uploadsRoot, crafted.slice('/uploads/events/'.length));
const escaped = path.relative(uploadsRoot, safePath);

console.log({ safePath, escaped });
console.log('safe guard should reject:', escaped.startsWith('..') || path.isAbsolute(escaped));
NODE

Repository: fabiodalez-dev/Pinakes

Length of output: 166


🏁 Script executed:

#!/bin/bash
node <<'NODE'
const path = require('path');

const uploadsRoot = path.resolve('/repo/public/uploads/events');
const crafted = '/uploads/events/../../../../tmp/pwned.txt';

const safePath = path.resolve(uploadsRoot, crafted.slice('/uploads/events/'.length));
const escaped = path.relative(uploadsRoot, safePath);

console.log({ safePath, escaped });
console.log('safe guard should reject:', escaped.startsWith('..') || path.isAbsolute(escaped));
NODE

Repository: fabiodalez-dev/Pinakes

Length of output: 166


🏁 Script executed:

#!/bin/bash
node <<'NODE'
const path = require('path');

const legacyBase = path.join('/repo/tests', '..', 'public');
const uploadsRoot = path.resolve('/repo/public/uploads/events');
const crafted = '/uploads/events/../../../../tmp/pwned.txt';

const legacyPath = path.join(legacyBase, crafted);
const safePath = path.resolve(uploadsRoot, crafted.slice('/uploads/events/'.length));
const escaped = path.relative(uploadsRoot, safePath);

console.log({ legacyPath, safePath, escaped });
console.log('legacy escapes uploads root:', !legacyPath.startsWith(uploadsRoot));
console.log('safe guard should reject:', escaped.startsWith('..') || path.isAbsolute(escaped));
NODE

Repository: fabiodalez-dev/Pinakes

Length of output: 236


🏁 Script executed:

#!/bin/bash
node <<'NODE'
const path = require('path');

const uploadsRoot = path.resolve(process.cwd(), 'public', 'uploads', 'events');
const crafted = '/uploads/events/../../../../tmp/pwned.txt';

const safePath = path.resolve(uploadsRoot, crafted.slice('/uploads/events/'.length));
const escaped = path.relative(uploadsRoot, safePath);

console.log({ safePath, escaped });
console.log('safe guard should reject:', escaped.startsWith('..') || path.isAbsolute(escaped));
NODE

Repository: fabiodalez-dev/Pinakes

Length of output: 184


🏁 Script executed:

#!/bin/bash
node <<'NODE'
const path = require('path');

const uploadsRoot = path.resolve('/repo/public/uploads/events');
const crafted = '/uploads/events/../../../../tmp/pwned.txt';

const safePath = path.resolve(uploadsRoot, crafted.slice('/uploads/events/'.length));
const escaped = path.relative(uploadsRoot, safePath);

console.log({ safePath, escaped });
console.log('safe guard should reject:', escaped.startsWith('..') || path.isAbsolute(escaped));
NODE

Repository: fabiodalez-dev/Pinakes

Length of output: 166


Teardown: impedire path traversal prima di fs.unlinkSync (cancellazioni fuori da public/uploads/events)

Nel teardown di tests/issue-137-event-image-layout.spec.js (~190-193) la guardia currentImage.startsWith('/uploads/events/') non impedisce traversal con ../../..; inoltre path.join(__dirname, '..', 'public', currentImage) con currentImage assoluto può risolvere fuori da public/uploads/events, rendendo unlinkSync potenzialmente distruttivo.

🔒 Fix minimale
-        if (currentImage && currentImage.startsWith('/uploads/events/')) {
-            const absPath = path.join(__dirname, '..', 'public', currentImage);
-            try {
-                fs.unlinkSync(absPath);
-            } catch (e) {
-                // ENOENT (already gone) is acceptable — the controller's
-                // own cleanup may have unlinked it on the success path.
-                // Any other error we surface to the test log but do not
-                // fail the teardown — the suite has finished otherwise.
-                if (e && e.code !== 'ENOENT') {
-                    console.warn(`teardown: could not unlink ${absPath}: ${e.message}`);
-                }
-            }
-        }
+        if (currentImage && currentImage.startsWith('/uploads/events/')) {
+            const uploadsRoot = path.resolve(__dirname, '..', 'public', 'uploads', 'events');
+            const relativeUploadPath = currentImage.slice('/uploads/events/'.length);
+            const absPath = path.resolve(uploadsRoot, relativeUploadPath);
+            const escaped = path.relative(uploadsRoot, absPath);
+            if (escaped.startsWith('..') || path.isAbsolute(escaped)) {
+                console.warn(`teardown: refusing to unlink path outside uploads root: ${currentImage}`);
+            } else {
+                try {
+                    fs.unlinkSync(absPath);
+                } catch (e) {
+                    if (e && e.code !== 'ENOENT') {
+                        console.warn(`teardown: could not unlink ${absPath}: ${e.message}`);
+                    }
+                }
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/issue-137-event-image-layout.spec.js` around lines 190 - 193, La
guardia attuale con currentImage.startsWith('/uploads/events/') non impedisce
path traversal e path.join può risolvere fuori dalla cartella prevista;
sostituisci il check con una normalizzazione e confronto assoluto: calcola
absBase = path.resolve(__dirname, '..', 'public', 'uploads', 'events') e absPath
= path.resolve(__dirname, '..', 'public', currentImage) (o rimuovi il prefisso
slash da currentImage prima di join), poi verifica che
absPath.startsWith(absBase + path.sep) prima di chiamare fs.unlinkSync(absPath);
usa queste variabili (currentImage, absPath, absBase, fs.unlinkSync,
path.resolve) per localizzare e applicare la correzione.

fabiodalez-dev added a commit that referenced this pull request May 22, 2026
…able tests

CodeRabbit fix on PR #141
-------------------------
attachSwalConfirm's confirmText default was unconditionally 'Elimina',
even when data-swal-confirm-kind="action" routed the dialog through
SwalApp.confirm (the neutral/gray helper). A form opting into
kind="action" without an explicit data-swal-confirm-button would render
a gray button labelled "Elimina" — the wrong destructive cue. Default
now follows the kind: action→Conferma, destructive→Elimina. Explicit
data-swal-confirm-button still wins for both.

UX follow-ups (3 non-destructive forms)
---------------------------------------
- admin/languages/index.php: set-default-language → kind=action
- utenti/dettagli_utente.php: activate-directly → kind=action
- utenti/index.php: activate-directly → kind=action

These three flows are reversible (re-set language, deactivate user)
and were showing a red destructive dialog. Now neutral/gray.

49 reusable Playwright tests
----------------------------
- tests/sweetalert2-comprehensive.spec.js (25 tests):
    Block A — window.SwalApp bus contract (6)
    Block B — attachSwalConfirm auto-wire / idempotence / proceed-marker (5)
    Block C — kind routing + confirmText defaults (3, validates this fix)
    Block D — i18n & data-attr propagation (3)
    Block E — refactored views per-page assertions (8, login-gated, skip-clean)

- tests/pr-139-event-image-additional.spec.js (11 tests):
    Block S — controller orphan-cleanup symmetry + path-traversal hardening
              + locale completeness + settings/route contract (9)
    Block H — admin-side HTTP contract checks (2, login-gated)

- tests/pr-136-archives-ric-cm-additional.spec.js (13 tests):
    Migration version-le-release invariant (project rule #6),
    ensureSchema() pattern compliance, plugin.json bumps, RiC-O entity
    coverage, README freshness. Reads files from
    origin/feat/ric-cm-archives via 'git show', so the suite works from
    any branch without checking out.

Validation
----------
- 39 no-login tests run green in ~30s
- 10 login-gated tests skip cleanly when E2E_ADMIN_PASS is absent
- All 13 pr-136 tests pass against origin/feat/ric-cm-archives

Refs: PR #141 (closes #140), PR #139 (closes #137), PR #136 (closes #122)
@fabiodalez-dev
Copy link
Copy Markdown
Owner Author

Superseded by #144 (combined/release-unified) — the unified release branch merges this work alongside the other two open PRs into a single release. All commits from this branch are preserved in the merge; CI green on the combined branch.

This PR is closed, not merged — the branch feat/event-image-layout-setting is retained for review history. Close ref: #144.

fabiodalez-dev added a commit that referenced this pull request May 27, 2026
release: unified roadmap — SwalApp + event layout + RiC-CM (#136 + #139 + #141)
ctariel pushed a commit to ctariel/Pinakes that referenced this pull request Jun 4, 2026
…#141 + fabiodalez-dev#139) into unified release

Brings in:
  - PR fabiodalez-dev#141 (closes fabiodalez-dev#140): full SweetAlert2 popup unification — every
    native window.alert/confirm/prompt routed through a single SwalApp
    helper bus with defensive native fallback. 11+ form/onsubmit/onclick
    refactors via the `data-swal-confirm` attribute pattern.
  - PR fabiodalez-dev#139 (closes fabiodalez-dev#137): admin-configurable event image layout —
    4 presets (full / banner / contained / thumb) on
    /eventi/<slug>, picker in admin/settings?tab=cms.
  - Validation work that landed during the review pass:
    - CodeRabbit-driven fix: confirmText default follows the kind
      (action→Conferma, destructive→Elimina) instead of always
      defaulting to Elimina (commit 383b0a2)
    - UX follow-ups: set-default-language + activate-user → kind=action
      (commit 450ed8e)
    - 49 reusable Playwright regression tests:
        sweetalert2-comprehensive.spec.js (25)
        pr-139-event-image-additional.spec.js (11)
        pr-136-archives-ric-cm-additional.spec.js (13)
    - Block E selector hardening for real admin URLs + DB seed state
      (commit 5701c58)
    - Phase 21 (full-test.spec.js) language-switch tests adapted to
      the SwalApp confirmation modal (commit 318117b)
    - CodeRabbit-driven fix on firstDeletableId: check ALL dep keys
      (children_count AND libri_count for generi) rather than just
      one (commit dcf98a0)

PR fabiodalez-dev#139 commits are already included in fabiodalez-dev#141's stack — no duplication.

Refs: PR fabiodalez-dev#141 (closes fabiodalez-dev#140), PR fabiodalez-dev#139 (closes fabiodalez-dev#137)

# Conflicts:
#	locale/de_DE.json
#	locale/en_US.json
#	locale/fr_FR.json
#	locale/it_IT.json
ctariel pushed a commit to ctariel/Pinakes that referenced this pull request Jun 4, 2026
…+ fabiodalez-dev#139 + fabiodalez-dev#136

Adds tests/unified-release-regression.spec.js — a focused regression
suite that targets the user-observable contract each of the 3
combined PRs introduces. Designed to sit alongside the existing
per-PR specs (sweetalert2-comprehensive, pr-139-event-image-additional,
pr-136-archives-ric-cm-additional, full-test, etc.) as a fast,
re-runnable safety net for the release branch.

Distribution (25 tests):
  Block U-SWAL  (8) — PR fabiodalez-dev#141 popup unification
    S1: SwalApp method surface
    S2: confirmText default branches on isAction (CodeRabbit fix)
    S3: defensive native fallback when Swal absent
    S4: one-shot data-swal-proceed marker (not attribute mutation)
    S5: 3 non-destructive forms set kind="action" (UX follow-up)
    S6: it_IT.json carries SwalApp default copy
    H1: window.SwalApp initialised on admin pages
    H2: data-swal-confirm forms auto-attached on /admin/utenti

  Block U-EVT   (8) — PR fabiodalez-dev#139 event image layout (fabiodalez-dev#137)
    S1: 4-preset enum in settings/index.php
    S2: event-detail.php renders event-cover--<preset>
    S3: SettingsController validates with in_array
    S4: 4 deleteUploadedImageFile lifecycle callsites
    S5: path-traversal hardening (prefix + realpath containment)
    S6: @media restyle for thumb layout
    H1: /admin/settings?tab=cms renders the layout picker
    H2: form posts to /admin/settings/events with CSRF

  Block U-RIC   (9) — PR fabiodalez-dev#136 RiC-CM full roadmap (fabiodalez-dev#122)
    S1: phase 2/3/4 + cleanup migration files
    S2: version.json ≥ highest migration version
    S3: 0.7.09.sql creates Phase 3 tables idempotently
    S4: 0.7.10.sql creates Phase 4 places + relations
    S5: ArchivesPlugin ensureSchema() called from BOTH hooks
    S6: RicJsonLdBuilder covers RecordResource/Agent/Activity/Place
    S7: OaiPmhServerPlugin handles metadataPrefix=ric-o
    S8: archives plugin.json ≥ 1.5.0, oai-pmh-server ≥ 1.1.0
    H1: /archives/collection.ric.json returns JSON-LD

Design choices:
  - 16 tests run without admin login (static checks on repo files +
    one no-auth HTTP smoke for the public RiC-O endpoint).
  - 9 tests require login — they test.skip() cleanly on missing
    credentials, wrong password, or missing seed state (legitimate
    skip, never fail).
  - No state mutation — every assertion reads HTML / JSON / static
    files. Re-runnable in any order, parallel-safe.

Run:
  /tmp/run-e2e.sh tests/unified-release-regression.spec.js \
    --config=tests/playwright.config.js --workers=1

Validated locally with E2E_ADMIN_PASS set: 24 passed, 1 skipped
(U-SWAL-H2 when /admin/utenti has no pending users — legitimate seed
state, not a test bug).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Events

2 participants