Skip to content

feat(archives): unified admin search now surfaces archival units#120

Merged
fabiodalez-dev merged 7 commits intomainfrom
feat/archives-unified-search
May 4, 2026
Merged

feat(archives): unified admin search now surfaces archival units#120
fabiodalez-dev merged 7 commits intomainfrom
feat/archives-unified-search

Conversation

@fabiodalez-dev
Copy link
Copy Markdown
Owner

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

Summary

  • SearchController::unifiedSearch() now applies a search.unified.sources filter hook after the existing books/authors/publishers queries, so active plugins can append their own result sets without touching the controller
  • ArchivesPlugin registers addArchivalSources for that hook in onActivate() (stored in plugin_hooks DB table, same pattern as app.routes.register)
  • addArchivalSources() searches archival_units via LIKE on constructed_title, formal_title, and scope_content (ISAD(G) 3.3.1) and returns results in the unified format with type: 'archive'
  • layout.php handles the new type: 'archive' in both desktop and mobile search dropdowns with a green archive icon (fas fa-archive) and the reference_code as the identifier sub-line

Design decisions

LIKE vs FULLTEXT: addArchivalSources uses LIKE instead of FULLTEXT. Short reference codes (IT-MI-1), year fragments (1943), and partial call numbers work correctly with LIKE but are silently dropped by MySQL FULLTEXT (minimum word length + stopword list). FULLTEXT is still used in the dedicated /admin/archives/search page where users write longer queries.

Hook pattern: The search.unified.sources filter hook was already documented in ArchivesPlugin::plannedHooks() — this commit promotes it from "planned" to "active". New plugins can contribute to unified search with zero changes to SearchController.

Test plan

  • Activate Archives plugin → create one archival unit
  • Type a matching word in the admin search bar → archive result appears with green icon and reference code
  • Deactivate Archives plugin → archive results disappear from unified search
  • Mobile search bar shows archive results too
  • Short queries (2 chars) work

Part of #103

Summary by CodeRabbit

  • Nuove Funzionalità
    • La ricerca unificata ora include risultati dagli archivi come fonte aggiuntiva (fino a 5 risultati aggiunti dai plugin).
    • I risultati archivio mostrano un'icona dedicata e l'identificatore nell'anteprima.
    • La ricerca considera titoli formali, titoli costruiti e descrizioni di contenuto per gli archivi.
    • La risposta finale di ricerca è limitata a un massimo di 20 risultati.

…ugin)

The admin global search bar now surfaces archival_units alongside books,
authors, and publishers when the Archives plugin is active.

Implementation:
- SearchController::unifiedSearch() applies a new filter hook
  `search.unified.sources` after the existing publishers query, allowing
  active plugins to append their own results to the JSON array.
- ArchivesPlugin registers `addArchivalSources` for that hook in onActivate()
  and inserts it in the plugin_hooks DB table so the hook survives server
  restarts (same pattern as app.routes.register / admin.menu.render).
- addArchivalSources() uses LIKE on constructed_title + formal_title +
  scope_content (ISAD(G) 3.3.1) instead of FULLTEXT: short reference codes
  ("IT-MI-1") and year fragments ("1943") are found reliably regardless of
  MySQL's minimum full-text word length.
- layout.php handles the new type:"archive" in both the desktop and mobile
  search dropdowns with a green archive icon and reference_code as identifier.

Closes part of #103 (unified search milestone).
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 1, 2026

Warning

Rate limit exceeded

@fabiodalez-dev has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 38 minutes and 52 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 38c5e3b5-3394-4088-a92d-5be8206582f4

📥 Commits

Reviewing files that changed from the base of the PR and between 3883c8d and 2693d54.

📒 Files selected for processing (5)
  • app/Support/PluginManager.php
  • storage/plugins/archives/ArchivesPlugin.php
  • tests/archives-plugin.unit.php
  • tests/plugin-manager.unit.php
  • tests/same-version-hook-sync.spec.js
📝 Walkthrough

Walkthrough

Il controller di ricerca unificata tronca i risultati core a 15 elementi, invoca l'hook search.unified.sources per permettere ai plugin di estendere i risultati, poi applica un cap finale di 20 risultati. L'ArchivesPlugin registra il filtro dentro una transazione DB, aggiunge il metodo pubblico addArchivalSources che fornisce fino a 5 risultati da archival_units. La view rende item.type === 'archive' mostrando anche item.identifier.

Changes

Unified search + Archives plugin + UI

Layer / File(s) Summary
Controller — Limiting & Hook
app/Controllers/SearchController.php
Dopo aggregazione core (books/authors/publishers) i risultati core sono troncati a 15 elementi prima di chiamare \App\Support\Hooks::apply('search.unified.sources', $results, [$q]); dopo i hook viene applicato array_slice($results, 0, 20) per il cap finale.
Plugin activation wiring (transactional)
storage/plugins/archives/ArchivesPlugin.php
onActivate() avvolge la registrazione degli hook in una transazione DB: registra app.routes.register, admin.menu.render e search.unified.sourcesaddArchivalSources (priority 10); commit su successo, rollback e rilancio su throwable.
Plugin — DB error handling
storage/plugins/archives/ArchivesPlugin.php
registerHookInDb() ora lancia RuntimeException su fallimenti di prepare/execute dell'INSERT invece di loggare e ritornare, rendendo l'attivazione atomica sotto transazione.
Plugin — Source provider
storage/plugins/archives/ArchivesPlugin.php
Aggiunto public function addArchivalSources(array $results, string $q): array che no-op su query vuota, altrimenti esegue una prepared LIKE su archival_units (constructed_title, formal_title, scope_content), limita a 5 righe, mappa e appende risultati con id, label, identifier, type='archive' e url.
UI — Rendering archive items
app/Views/layout.php
Desktop e mobile search rendering aggiungono case 'archive' con icona/colore specifici e mostrano item.identifier escapato se presente.
Plugin metadata
storage/plugins/archives/plugin.json
Bump versione plugin da 1.0.0 a 1.0.1.
PluginManager — Activation instance
app/Support/PluginManager.php
Al aggiornamento di plugin bundled, onActivate() viene ora eseguito sull'istanza creata da instantiatePlugin(...) (se il plugin è marcato active), con try/catch che logga warning su fallimento.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend
    participant Controller
    participant Hooks
    participant ArchivesPlugin as Plugin
    participant DB

    User->>Frontend: invia query q
    Frontend->>Controller: GET /search?q
    Controller->>DB: query core sources (books, authors, publishers)
    DB-->>Controller: core results
    Controller->>Controller: slice(core results, 0, 15)
    Controller->>Hooks: apply('search.unified.sources', results, [q])
    Hooks->>Plugin: call addArchivalSources(results, q)
    Plugin->>DB: prepared LIKE query su archival_units
    DB-->>Plugin: fino a 5 corrispondenze
    Plugin->>Plugin: mappa corrispondenze (id,label,identifier,type='archive',url)
    Plugin-->>Hooks: return risultati augmentati
    Hooks-->>Controller: risultati finali
    Controller->>Controller: slice(results, 0, 20)
    Controller-->>Frontend: JSON response con risultati
    Frontend->>User: render (elementi archive mostrano identifier & icona)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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: l'aggiunta della ricerca unificata per le unità archiviali nel controllore di ricerca.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/archives-unified-search

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
Review rate limit: 0/1 reviews remaining, refill in 38 minutes and 52 seconds.

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

@fabiodalez-dev
Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

✅ Actions performed

Full review triggered.

@fabiodalez-dev
Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

✅ Actions performed

Full review triggered.

fabiodalez-dev added a commit that referenced this pull request May 2, 2026
…AD3, fix broken links

- Fix broken links: create guida/arricchimento.md and en/guide/bulk-enrichment.md
- Add guida/dischi.md + en/guide/discs.md: media types, dynamic labels, Discogs/MusicBrainz/Deezer plugins
- Add guida/collane.md + en/guide/series.md: series hierarchy (cycles, seasons, spin-offs) from v0.5.9.6
- Update guida/archivi.md + en/guide/archives.md: unified search (PR #120) + interoperability section (OAI-PMH 2.0, Dublin Core XML, EAD3 bulk export — PR #127)
- Update _sidebar.md + en/_sidebar.md: add Dischi/Discs and Collane/Series entries
- Update changelog: add v0.5.9.6 entry (series hierarchy) and "In arrivo" section (PR #118/119/120/127)
- Update tecnico/requisiti.md + en/technical/requirements.md: note on zip extension installer check (PR #118)
@fabiodalez-dev
Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

✅ Actions performed

Full review triggered.

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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Controllers/SearchController.php`:
- Around line 202-204: The plugin-added results from
\App\Support\Hooks::apply('search.unified.sources', ...) are being appended to
$results but then lost by the later array_slice(..., 0, 20); to fix, call
Hooks::apply into a separate variable (e.g. $pluginResults) instead of appending
directly to $results, decide on a reserved slot count (e.g. $reservedForPlugins
= 3), trim the core $results (from SearchController) to max 20 -
$reservedForPlugins, then merge $pluginResults into the trimmed core results and
only then apply array_slice(..., 0, 20) to produce the final list so extension
results are guaranteed reserved slots.

In `@storage/plugins/archives/ArchivesPlugin.php`:
- Around line 144-147: The hook registrations in ArchivesPlugin:: (calls to
registerHookInDb) are not atomic and may leave the plugin partially registered;
wrap the multiple registerHookInDb calls in a database transaction/atomic block
(beginTransaction/commit) and if any registerHookInDb fails, roll back and
rethrow the exception so the caller sees the error; specifically modify the
method that registers these hooks (the block containing
registerHookInDb('app.routes.register', 'registerRoutes', 10),
registerHookInDb('admin.menu.render', 'renderAdminMenuEntry', 10),
registerHookInDb('search.unified.sources', 'addArchivalSources', 10)) to start a
transaction before the first call, commit after all succeed, and catch
exceptions to rollback and rethrow.
🪄 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: 123bbd92-cfed-43d5-bd6e-1724435b4183

📥 Commits

Reviewing files that changed from the base of the PR and between 18c168c and 1f3c05e.

📒 Files selected for processing (3)
  • app/Controllers/SearchController.php
  • app/Views/layout.php
  • storage/plugins/archives/ArchivesPlugin.php

Comment thread app/Controllers/SearchController.php Outdated
Comment thread storage/plugins/archives/ArchivesPlugin.php Outdated
…ration

- SearchController: pre-slice core results to 15 before Hooks::apply so
  plugin sources (archives) are not systematically truncated by the global
  cap of 20 (fixes CodeRabbit finding — array_slice after hook was a no-op)
- ArchivesPlugin::onActivate: wrap 3 registerHookInDb calls in a single
  transaction so partial activation is impossible on DB error
@fabiodalez-dev
Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

✅ Actions performed

Full review triggered.

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)
storage/plugins/archives/ArchivesPlugin.php (1)

144-153: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

La transazione qui non è davvero atomica.

registerHookInDb() continua a loggare e tornare senza lanciare: se uno degli insert fallisce, il catch non scatta e il commit() può confermare una registrazione parziale.

Proposta di fix
-        if ($stmt === false) {
-            SecureLogger::error(...);
-            return;
-        }
+        if ($stmt === false) {
+            SecureLogger::error(...);
+            throw new \RuntimeException(...);
+        }

-        if (!$stmt->execute()) {
-            SecureLogger::error(...);
-        }
+        if (!$stmt->execute()) {
+            SecureLogger::error(...);
+            throw new \RuntimeException(...);
+        }

As per coding guidelines storage/plugins/**: Hook registration: transazione + rethrow on failure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@storage/plugins/archives/ArchivesPlugin.php` around lines 144 - 153, The
transaction around the hook registration is not atomic because
registerHookInDb() logs and returns on failure instead of throwing, so commit()
can succeed after partial inserts; change the flow so failures trigger rollback
by either making registerHookInDb() throw on error or by checking its return
value in the transaction block and throwing an exception when it returns false;
update the transaction code that calls begin_transaction(),
registerHookInDb('app.routes.register', ...),
registerHookInDb('admin.menu.render', ...),
registerHookInDb('search.unified.sources', ...) to detect any failure and
rethrow (so rollback() runs) before calling commit(), ensuring atomic hook
registration.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@storage/plugins/archives/ArchivesPlugin.php`:
- Around line 144-153: The transaction around the hook registration is not
atomic because registerHookInDb() logs and returns on failure instead of
throwing, so commit() can succeed after partial inserts; change the flow so
failures trigger rollback by either making registerHookInDb() throw on error or
by checking its return value in the transaction block and throwing an exception
when it returns false; update the transaction code that calls
begin_transaction(), registerHookInDb('app.routes.register', ...),
registerHookInDb('admin.menu.render', ...),
registerHookInDb('search.unified.sources', ...) to detect any failure and
rethrow (so rollback() runs) before calling commit(), ensuring atomic hook
registration.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d2f0f818-afc5-4b10-99fe-18d968ce4ba2

📥 Commits

Reviewing files that changed from the base of the PR and between 18c168c and f1d9d68.

📒 Files selected for processing (3)
  • app/Controllers/SearchController.php
  • app/Views/layout.php
  • storage/plugins/archives/ArchivesPlugin.php

The begin_transaction/commit/rollback wrapper in onActivate() was not
truly atomic because registerHookInDb() swallowed errors via log+return.
Now both failure paths (prepare() == false, execute() == false) log AND
throw so the catch block in onActivate() actually triggers rollback.
@fabiodalez-dev
Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

✅ Actions performed

Full review triggered.

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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Support/PluginManager.php`:
- Around line 120-133: L'upgrade viene già scritto nella tabella plugins prima
di chiamare onActivate() (qui quando chiami instantiatePlugin(...) e poi
->onActivate()), quindi se onActivate fallisce bisogna impedire che l'upgrade
venga considerato riuscito: nella catch(\Throwable $e) al posto di limitarsi a
SecureLogger::warning(...) ripristina lo stato precedente (es. la colonna
version del plugin al valore precedente $dbVersion o imposta is_active a 0
usando l'id del plugin passato a instantiatePlugin) e poi fallisci
esplicitamente l'operazione (rilanciando l'eccezione o restituendo/propagando un
errore) in modo che version_compare() ritenti l'upgrade al prossimo boot; fai
riferimento a instantiatePlugin, onActivate e alla catch corrente per collocare
il rollback.

In `@storage/plugins/archives/ArchivesPlugin.php`:
- Around line 4019-4031: The unified search query omits reference_code from the
LIKE conditions so searches for codes like "IT-MI-1" never match; update the SQL
in ArchivesPlugin (the prepared statement selecting id, reference_code,
constructed_title from archival_units) to include "OR reference_code LIKE ?" in
the WHERE clause and adjust the bind_param call to pass the $s parameter for
that additional placeholder (i.e., add another 's' and another $s argument) so
reference_code is searched with the same wildcard pattern as
constructed_title/formal_title/scope_content.
🪄 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: df0280fc-9cc7-40cd-816f-f4f5b865883f

📥 Commits

Reviewing files that changed from the base of the PR and between f1d9d68 and 3883c8d.

📒 Files selected for processing (3)
  • app/Support/PluginManager.php
  • storage/plugins/archives/ArchivesPlugin.php
  • storage/plugins/archives/plugin.json

Comment thread app/Support/PluginManager.php
Comment thread storage/plugins/archives/ArchivesPlugin.php Outdated
When a bundled plugin is re-deployed with the same version already in
the DB, autoRegisterBundledPlugins skipped onActivate because
version_compare reported no change. Any hooks added to the codebase
between the two deploys would be silently absent from plugin_hooks.

Add an elseif branch for diskVersion === dbVersion + is_active: calls
onActivate (which runs ensureSchema, a DELETE+INSERT idempotent cycle)
so the DB always reflects the current hook set. Failure is non-fatal
(logs a warning, does not rethrow) since running with stale hooks
is better than breaking the admin plugin page entirely.

Regression test added to tests/plugin-manager.unit.php.
Playwright spec used by reinstall-test.sh step A.7 to functionally
exercise the autoRegisterBundledPlugins elseif branch:

1. reinstall-test.sh activates archives plugin via SQL and wipes its
   plugin_hooks rows (simulating a same-version re-deploy with new hooks)
2. This spec logs in as admin and navigates to /admin/plugins, which
   calls getAllPlugins() → autoRegisterBundledPlugins()
3. reinstall-test.sh then verifies via MySQL that all hooks were restored

Confirms the fix works end-to-end without any deactivate/reactivate.
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.

1 participant