{"id":40844166,"url":"https://github.com/fabiodalez-dev/pinakes","last_synced_at":"2026-07-03T09:01:24.230Z","repository":{"id":325777147,"uuid":"1054452832","full_name":"fabiodalez-dev/Pinakes","owner":"fabiodalez-dev","description":"Complete Library Management System (ILS) - Circulation, OPAC, cataloging, reservations, loans, automatic notifications, plugin system, and Z39.50/SRU server","archived":false,"fork":false,"pushed_at":"2026-06-30T22:24:19.000Z","size":41900,"stargazers_count":78,"open_issues_count":9,"forks_count":6,"subscribers_count":3,"default_branch":"main","last_synced_at":"2026-07-01T00:15:07.334Z","etag":null,"topics":["biblioteche","books","catalog","cataloging","circulation","dewey-decimal","digital-library","ebooks","ils","isbn","koha-alternative","library","library-catalog","library-management","opac","open-source","self-hosted"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fabiodalez-dev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-10T21:28:10.000Z","updated_at":"2026-06-30T16:52:34.000Z","dependencies_parsed_at":null,"dependency_job_id":"6804e40f-1f30-4f7e-abaf-2ce8aa7c5d2f","html_url":"https://github.com/fabiodalez-dev/Pinakes","commit_stats":null,"previous_names":["fabiodalez-dev/pinakes"],"tags_count":72,"template":false,"template_full_name":null,"purl":"pkg:github/fabiodalez-dev/Pinakes","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fabiodalez-dev%2FPinakes","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fabiodalez-dev%2FPinakes/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fabiodalez-dev%2FPinakes/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fabiodalez-dev%2FPinakes/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fabiodalez-dev","download_url":"https://codeload.github.com/fabiodalez-dev/Pinakes/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fabiodalez-dev%2FPinakes/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35079379,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-03T02:00:05.635Z","response_time":110,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["biblioteche","books","catalog","cataloging","circulation","dewey-decimal","digital-library","ebooks","ils","isbn","koha-alternative","library","library-catalog","library-management","opac","open-source","self-hosted"],"created_at":"2026-01-21T23:10:24.716Z","updated_at":"2026-07-03T09:01:24.217Z","avatar_url":"https://github.com/fabiodalez-dev.png","language":"JavaScript","funding_links":["https://buymeacoffee.com/fabiodalez"],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./public/assets/brand/social.jpg\" alt=\"Pinakes - Library Management System\" width=\"800\"\u003e\n\u003c/p\u003e\n\n# Pinakes\n\n\u003e **Open-Source Integrated Library System**\n\u003e License: GPL-3  |  Languages: Italian, English, French, German\n\nPinakes is a self-hosted, full-featured ILS for schools, municipalities, and private collections. It focuses on automation, extensibility, and a usable public catalog without requiring a web team.\n\n📱 **Pinakes ships with a free companion [Android app](https://github.com/fabiodalez-dev/Pinakes-Android).** Enable the bundled Mobile API plugin, point the app at your instance URL, and your members can browse the catalog, check real availability, borrow \u0026amp; reserve, read ebooks / listen to audiobooks, and manage their loans from the phone.\n\n[![Version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Ffabiodalez-dev%2FPinakes%2Fmain%2Fversion.json\u0026query=%24.version\u0026label=version\u0026style=for-the-badge\u0026color=0ea5e9)](version.json)\n[![Installer Ready](https://img.shields.io/badge/one--click_install-ready-22c55e?style=for-the-badge\u0026logo=azurepipelines\u0026logoColor=white)](installer)\n[![License](https://img.shields.io/badge/License-GPL--3.0-orange?style=for-the-badge)](LICENSE)\n\n[![PHP](https://img.shields.io/badge/PHP-8.2%2B-777BB4?style=for-the-badge\u0026logo=php\u0026logoColor=white)](https://www.php.net/)\n![Slim](https://img.shields.io/badge/slim%20framework-2C3A3A?style=for-the-badge\u0026logo=slim\u0026logoColor=white)\n[![MySQL](https://img.shields.io/badge/mysql-4479A1.svg?style=for-the-badge\u0026logo=mysql\u0026logoColor=white)](https://www.mysql.com/)\n[![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge\u0026logo=javascript\u0026logoColor=%23F7DF1E)](https://developer.mozilla.org/docs/Web/JavaScript)\n![TailwindCSS](https://img.shields.io/badge/tailwindcss-0ea5e9?style=for-the-badge\u0026logo=tailwindcss\u0026logoColor=white)\n\n[![Android app](https://img.shields.io/badge/Android%20app-Pinakes-3DDC84?style=for-the-badge\u0026logo=android\u0026logoColor=white)](https://github.com/fabiodalez-dev/Pinakes-Android)\n[![Documentation](https://img.shields.io/badge/Documentazione-Docsify-4285f4?style=for-the-badge\u0026logo=readthedocs\u0026logoColor=white)](https://fabiodalez-dev.github.io/Pinakes/)\n[![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge\u0026logo=buy-me-a-coffee\u0026logoColor=black)](https://buymeacoffee.com/fabiodalez)\n\n---\n\n## Requirements\n\n- **PHP 8.2 or higher** — matches `composer.json` (`^8.2`); the installer and the in-app updater enforce this floor.\n- **MySQL 5.7+** or **MariaDB 10.3+**\n- **PHP extensions**: PDO, PDO MySQL, MySQLi, Mbstring, JSON, GD, Fileinfo, Zip\n- **Web server**: Apache (`mod_rewrite` + `.htaccess`) or nginx\n- No Composer or Node build tools are needed on the production host — frontend assets are precompiled and `vendor/` ships inside the release ZIP.\n\n---\n\n## What's New in v0.7.27\n\nA follow-up to the 0.7.26 updater hardening, from a real upgrade report ([#205](https://github.com/fabiodalez-dev/Pinakes/issues/205)).\n\n### Clearer \"not writable\" reporting in the updater\n\n- The update panel's **system-requirements** list used to label each writable-permission check with only the folder's base name, so an install whose root directory is called `html` (common on NAS/QNAP web roots) showed a cryptic **\"Write: html — Not writable\"** with no way to tell which directory was meant. Each check now shows **what it is plus the full path** — e.g. `Write — Installation root (/share/…/html)`, `Write — Storage directory (…/storage)` — so the operator knows exactly which directory to make writable by the web-server user. New i18n keys added to all four locales.\n\n### One-shot permissions fixer (`bin/setup-permissions.sh`)\n\n- A CLI script that fixes **all** filesystem permissions the app and the in-app updater need, in a single run — the reliable cure for \"Update failed: Unable to create directory / Not writable\" on shared hosting and NAS devices. It **auto-detects the web-server (PHP) user**, hands it ownership of the **whole install tree** (the updater must create/overwrite files, so owning the root — not just a few sub-folders — is what actually matters), then applies safe modes: no `chmod 777`, executables preserved, `.env` kept non-world-readable.\n- **Dry-run by default** (shows exactly what it would do); `--apply` to run; `--user` / `--group` / `--root` overrides for any host. QNAP example: `sudo bin/setup-permissions.sh --apply --user httpdusr`.\n\nNo schema migration. Both changes are operational; upgrading from 0.7.26 changes nothing but the requirements labels and adds the helper script.\n\n---\n\n## What's New in v0.7.26\n\nBook reviews reach the mobile app, the loan/reservation system gets a full review pass, plus an email-template migration.\n\n### Book reviews in the Android app ([#209](https://github.com/fabiodalez-dev/Pinakes/pull/209))\n\n- **The mobile API now serves book reviews** (stars + text): `GET/PUT/DELETE /api/v1/catalog/books/{id}/reviews` and `GET /api/v1/me/reviews`, gated by a per-instance `reviews` feature flag (off in catalogue mode).\n- **Only borrowers can review.** Writing a review requires a past or present loan of that title (`403 not_eligible` otherwise); `PUT` is an idempotent upsert (one review per user + book).\n- **Moderation stays authoritative.** A new or edited review returns to *pending*; aggregates and other users' reviews count approved reviews only, while the author always sees their own. The bundled `mobile-api` plugin moves to `1.1.0`. No schema migration — the `recensioni` table has shipped since the first release.\n\n### Loan \u0026 reservation system review ([#207](https://github.com/fabiodalez-dev/Pinakes/pull/207), #205)\n\n- A full review pass over the loan/reservation flow: 26 findings fixed (availability recalculation, reservation-queue edge cases, return-to-repair handling) plus updater hardening (a preflight writability dry-run that aborts before touching any file, and self-healing of owned permissions).\n\n### UI\n\n- **Sidebar** ([#208](https://github.com/fabiodalez-dev/Pinakes/pull/208)): the logo is stacked above the title for a cleaner header.\n\n**Migration** (`migrate_0.7.26.sql`): seeds the email templates introduced by the loan review (including the new `reservation_cancelled` template) for **all shipped locales**, adds the `loans.max_loan_duration_days` setting, and fixes a single-brace placeholder in the `loan_overdue_admin` subject. Fully idempotent — `INSERT IGNORE` never overwrites admin-customised rows, existing settings are kept, and legacy-schema installs are upgraded in place via information_schema-gated guards.\n\n---\n\n## What's New in v0.7.25\n\nSix fixes integrated from the open pull requests, plus a schema migration.\n\n### Book form field types + copy repair ([#203](https://github.com/fabiodalez-dev/Pinakes/pull/203))\n\n- **Acquisition type is now free text.** `tipo_acquisizione` was an `enum('acquisto','donazione')`, so anything typed outside those two (e.g. \"Deposito legale\", \"Scambio\") was silently reset to the default on save. The column is now `VARCHAR(50)` and stores what the form accepts; existing values are preserved.\n- **Copies can go into repair on return.** When returning a loan you can mark the copy as *in maintenance* or *in restoration*: the loan closes as returned, the copy is held out of circulation until an operator restores it, and the borrower still receives the return notification.\n- **New derived availability state.** A book whose copies are all out of circulation is now labelled **non disponibile** by the availability engine instead of showing a stale flag — localised everywhere (catalogue, book/author/publisher pages) with its own badge colour.\n\n### Reservations, scraping \u0026 i18n\n\n- **Waitlist promotion** ([#199](https://github.com/fabiodalez-dev/Pinakes/pull/199), #157): when reservations fully subscribe a title's copies, the head of the waitlist is promoted correctly as copies free up.\n- **Scraped translator/illustrator no longer overwrite manual entries** ([#200](https://github.com/fabiodalez-dev/Pinakes/pull/200)): a submit carrying both a typed and a scraped value keeps the librarian's typed value.\n- **Publication date is free text** ([#202](https://github.com/fabiodalez-dev/Pinakes/pull/202), #201): the field no longer claims a fixed \"Italian format\"; it accepts any text, in all four locales.\n\n### Mobile \u0026 API\n\n- **Loan date picker on mobile** ([#198](https://github.com/fabiodalez-dev/Pinakes/pull/198)): the loan calendar forces flatpickr's own picker on phones instead of the native control.\n- **Self-hosted Swagger UI** ([#197](https://github.com/fabiodalez-dev/Pinakes/pull/197)): the API docs page (`/api/v1/docs`) serves its assets locally, with no CDN dependency.\n\n**Migration** (`migrate_0.7.25-rc.1.sql`): widens `tipo_acquisizione` to `VARCHAR(50)` and adds `non_disponibile` to the `stato` enum — idempotent, existing data preserved. The **mobile API contract is unchanged** (availability is a computed four-value state from copy counts, never the raw `stato`), so **the Android app needs no update**.\n\n## What's New in v0.7.24\n\n### Backend UI homogenisation ([#196](https://github.com/fabiodalez-dev/Pinakes/pull/196))\n\nA pass over the admin interface so every page looks and behaves consistently.\n\n- **One accent colour everywhere.** Plugin pages used a mix of blue, purple and indigo; they are now all the same blue as the core admin chrome — settings pages (Mobile API, Discogs, GoodLib) and the feature pages of Archives, NCIP and Digital Library. Tailwind now also scans plugin views, so plugin classes are compiled reliably instead of depending on overlap with app views.\n- **Readable settings on mobile.** Below 640px the settings pages (app and plugin) go flat — no card boxes, a single side gutter, no doubled padding — so content is wide and legible. Desktop is unchanged. The two odd-one-out section headers in the Advanced tab (Sitemap, Public API) were aligned to the same heading style as every other section.\n- **All toggles are now identical.** The oversized OFF/ON switch on the API setting, the events visibility toggle and the bulk-enrich switch were unified to the standard grey→dark switch used across the app. A new end-to-end test turns every toggle on to guard against regressions.\n- **Clearer sitemap field.** The settings page now makes it obvious which value to submit to Google Search Console (the public URL, with a Copy button); the filesystem path is labelled as server-only.\n- **Mobile sidebar no longer scrolls the page behind it.** Opening the mobile menu now locks the page underneath the overlay (it previously kept scrolling on iOS).\n\nThis is a **code-only release — no new migration**. The Mobile API plugin moves to **1.0.2**; no other bundled plugin changed.\n\n## What's New in v0.7.23\n\n### Mobile API ⇄ Android app coherence ([#194](https://github.com/fabiodalez-dev/Pinakes/pull/194))\n\nThis release closes the gaps between the bundled **Mobile API** plugin and the companion Android app so the two actually agree on every contract.\n\n- **The \"mobile access disabled\" dead end is gone.** The Mobile API access flag lives in the plugin's own settings, but there was no way to reach them from the admin UI — so an upgraded site that activated the plugin still answered the app with *\"mobile access is disabled on this library.\"* **Admin → Plugins** now renders a generic **Settings** action for any plugin that ships a settings view, and posts back to the plugin that owns it. Open **Mobile API → Settings** and flip the access toggle.\n- **Loan vs. reservation now matches the website.** A request with no date (or today's date) on an available copy is booked as an **immediate loan**, exactly like the web form; a future date becomes a **reservation**. The app and the website no longer disagree about what \"borrow now\" does.\n- **Availability calendar normalised.** Per-day availability is exposed with a consistent `free` / `partial` / `full` state so the app's date-picker colours match the real stock.\n- **OpenAPI contract realigned** to the controllers it documents — including the `/messages` request schema, which now reflects the field the endpoint actually reads (`messaggio`, with the documented fallbacks) instead of a stale `subject`/`body` shape.\n\nOn the app side (the separate [Pinakes Android](https://github.com/fabiodalez-dev/Pinakes-Android) repo, [#5](https://github.com/fabiodalez-dev/Pinakes-Android/pull/5)) this ships the in-app **registration** and **password-recovery** screens, feature-flag gating that follows the instance's catalogue-only mode, and a round of security hardening (duplicate-submit guard on registration, no account-enumeration on password recovery, password-confirmation forwarded end to end).\n\n### Security — Slim CVE-2026-48157 ([#192](https://github.com/fabiodalez-dev/Pinakes/pull/192))\n\nBumped `slim/slim` 4.15.1 → 4.15.2 to clear **CVE-2026-48157**, a reflected XSS affecting Slim ≤ 4.15.1. Dependency-only bump within the existing major version — no application code changes.\n\nThis is a **code-only release — no new migration**. The Mobile API plugin moves to **1.0.1**; no other bundled plugin changed.\n\n## What's New in v0.7.22\n\n### Series / universe / cycle autocomplete on the book form ([#179](https://github.com/fabiodalez-dev/Pinakes/issues/179))\n\nThe cycle, universe, group and series fields on the book form are now **Choices.js autocompletes**, the same shape as the author and publisher pickers. Type a couple of letters and Pinakes suggests existing values instead of making you retype them — so a single different character no longer silently spawns a duplicate universe. Brand-new values can still be created (type and confirm). On an **upgrade**, the series you already use are suggested immediately (they are backfilled into the `collane` table); universes, groups and cycles are new dimensions and populate as you organise the hierarchy — there is nothing to import for them because the data never existed on older installs. No new migration: this is a code-only release.\n\n### Fixes\n\n- **Enter committed the wrong series value** ([#179](https://github.com/fabiodalez-dev/Pinakes/issues/179) review): when a suggestion was highlighted but you had typed something different, pressing Enter could commit the highlighted suggestion instead of your text — the same class of bug as the author field (#74). The series fields now use the same highlighted-vs-typed guard, covered by a dedicated regression test.\n- **View escaping** aligned to `htmlspecialchars(…, ENT_QUOTES, 'UTF-8')` on the new series fields, per project convention.\n- **Test hardening** for the series autocomplete (real Choices.js widget interaction + complete credential skip-guards).\n\n## What's New in v0.7.21\n\n### Mobile API + companion Android app ([#177](https://github.com/fabiodalez-dev/Pinakes/pull/177))\n\nPinakes now ships a versioned REST API for mobile clients. A bundled **Mobile API** plugin exposes `/api/v1` — instance discovery (`/health`), email/password login with bearer tokens, catalog browse \u0026amp; search, real availability, loans, reservations, wishlist, push notifications, and digital-asset streaming for ebooks/audiobooks. It is server-agnostic and adapts to each instance's settings (language, catalogue-only mode, push availability). On a **fresh install the plugin is active out of the box**, so `/api/v1` works immediately; on an **upgrade** it ships disabled — enable it from **Admin → Plugins → Mobile API** — to avoid changing behaviour on existing sites. Either way, review mobile app access in its settings.\n\nA free, native **[Pinakes Android app](https://github.com/fabiodalez-dev/Pinakes-Android)** (Kotlin / Jetpack Compose, Material 3) consumes this API: any library can point it at its own instance URL and hand it to members. Browse the catalog, check availability, borrow \u0026amp; reserve, read ebooks / listen to audiobooks, and manage loans from the phone. The bearer token is stored in `EncryptedSharedPreferences`; cleartext HTTP is permitted only for loopback and the emulator. A prebuilt debug APK is published on the [app's Releases page](https://github.com/fabiodalez-dev/Pinakes-Android/releases).\n\n### Upgrade upload limits — clear error instead of a misleading CSRF page\n\nWhen an upload (e.g. the ~30\u0026nbsp;MB admin-UI upgrade ZIP) exceeds the server's `post_max_size`, PHP discards the request body — CSRF token included — before the app runs. Instead of the confusing \"security check failed\" page, Pinakes now returns a clear **413** that points at the real cause and how to raise `post_max_size` / `upload_max_filesize` (note: `php_value` in `.htaccess` only applies under mod_php — on php-fpm/CGI hosts the host's PHP config must be raised).\n\n### Security — dependency CVE fixes\n\nBumped `guzzlehttp/guzzle` to 7.12.1 and `guzzlehttp/psr7` to 2.12.1 to clear three medium-severity advisories disclosed 2026-06-18: CVE-2026-55767 (dot-only cookie domains matching all hosts) and CVE-2026-55568 (silent HTTPS-proxy downgrade to cleartext) in guzzle, and CVE-2026-55766 (CRLF injection in HTTP start-line serialization) in psr7. No application code changes — patch/minor bumps within the existing major versions.\n\n## What's New in v0.7.20.2\n\nInternal release — **test coverage only, no functional change to the application**.\n\n- **Regression suite for the cover fix ([#173](https://github.com/fabiodalez-dev/Pinakes/issues/173))** — a 5-test end-to-end suite ([#176](https://github.com/fabiodalez-dev/Pinakes/pull/176)) that locks external-cover download \u0026amp; save (OpenLibrary → Internet Archive redirect) and the SSRF boundary on both the save path and the `/api/cover/download` endpoint, so the #173 fix can't silently regress again. Tests are excluded from the release ZIP (`tests/ export-ignore`), so this package is byte-identical to 0.7.20.1 apart from the version marker — no need to update unless you track the latest version.\n\n## What's New in v0.7.20.1\n\nMaintenance release — two fixes, no schema change.\n\n### Book covers from OpenLibrary now save ([#173](https://github.com/fabiodalez-dev/Pinakes/issues/173))\n\nOpenLibrary serves its cover images via a redirect from `covers.openlibrary.org` to the Internet Archive, which the cover downloader's strict host check blocked — so the cover previewed on the edit form but was dropped on save. Cover fetching was reworked to accept any public source while staying **SSRF-safe**: redirects are followed one hop at a time, every hop's host is resolved to a verified **public** IP and the connection is **pinned** to it (no DNS-rebind), and IPv4-mapped / NAT64 addresses are rejected. OpenLibrary (and any CDN-hosted) covers now download and are stored as a local file.\n\n### Hardened updater — fail-closed patches ([#174](https://github.com/fabiodalez-dev/Pinakes/pull/174))\n\nFollow-ups to the in-app updater: a present-but-broken pre-update patch now aborts the update (fail-closed) instead of silently proceeding un-patched; a post-install patch error is surfaced as a warning instead of being hidden; and the releases API fetch no longer follows redirects with the auth token. Internal-only robustness — no action needed on upgrade.\n\n---\n\n## What's New in v0.7.20\n\n### Author profiles — photo \u0026 sources ([#163](https://github.com/fabiodalez-dev/Pinakes/issues/163) / [#170](https://github.com/fabiodalez-dev/Pinakes/pull/170))\n\nAuthors can now carry a **photo** (uploaded with a live preview, or an external URL) and a list of **relevant source/website links**. The author and publisher admin detail pages were restyled to match the book page: a full-width identity card, a readable book catalog that gets its own full-width row, and the same buttons/chrome across all three entity pages.\n\n### Loans \u0026 reservations — canonical state model ([#171](https://github.com/fabiodalez-dev/Pinakes/pull/171))\n\nThe loan/reservation engine was unified around a single occupancy model, fixing **10 state bugs**: returned copies reliably become lendable again, multi-copy availability is computed correctly, the reservation waitlist occupies its promised period without false positives/negatives, and admin/dashboard/calendar displays stay consistent. A new `restituito_in_ritardo` flag records late returns.\n\n### Security — hardened in-app updater ([#172](https://github.com/fabiodalez-dev/Pinakes/pull/172))\n\nUpdate packages are now verified against GitHub's server-side **sha256 digest** (delivered over TLS) with a constant-time compare — a present-but-unverifiable asset blocks the update instead of being silently skipped. The GitHub bearer token is scoped to the API host and never follows a redirect, and pre/post-update patches go through the same digest verification. A post-install patch that can't be verified is now a non-fatal warning (the core update is already applied) rather than a misleading failure.\n\n### Security — session \u0026 login\n\nFive pre-existing session pitfalls that could log an admin out unexpectedly were closed: **logout is now a CSRF-protected POST** (a stray link can no longer log you out), the \"remember me\" auto-login no longer bounces concurrent tabs to the login screen, a CSRF token mismatch on a still-valid session shows a clear *\"reload and resubmit\"* page instead of a misleading *\"session expired\"*, and `storage/sessions` is created automatically so sessions never fall back to a `/tmp` that shared hosts purge early.\n\n### Migration \u0026 tooling\n\n- **`migrate_0.7.20.sql`** — author photo/link columns, the `restituito_in_ritardo` flag, and a one-time loan-state cleanup. Idempotent; applies on upgrade from any older stable (and from the `0.7.20-rc.1` prerelease).\n- CodeQL analysis is now scoped to application source (vendored bundles and test fixtures excluded), so the security dashboard reflects only our code.\n- Verified end-to-end: fresh-install and the real admin-UI upgrade path (`reinstall-test.sh` Test A + B) both pass, full lifecycle suite green, PHPStan level 5 clean.\n\n---\n\n## What's New in v0.7.19\n\n### Faceted catalog filters ([#169](https://github.com/fabiodalez-dev/Pinakes/pull/169))\n\nThe public catalog (`/catalogo`) filters were reworked to be clear and noise-free. Choosing a value **collapses** that facet to a removable pill and re-scopes the others; options that would return zero results disappear, single-value facets are suppressed, and the year range clamps to the data's real bounds. A new **Author** facet joins genres, publishers, media type and availability, long lists scroll internally with subtle borders, and everything is **theme-aware** (driven by the app's CSS variables).\n\n### Complete backup \u0026 restore ([#162](https://github.com/fabiodalez-dev/Pinakes/issues/162) / [#167](https://github.com/fabiodalez-dev/Pinakes/pull/167))\n\nA full backup system from **Admin → Updates/Maintenance**: it archives the database **and** the uploaded files, with a hash-verified, streaming restore (fail-loud staging/promotion, 4xx restore errors, admin-only download/delete). Restores replace the database content with the archive's — only restore trusted archives.\n\n### Scanner \u0026 cover fixes ([#164](https://github.com/fabiodalez-dev/Pinakes/issues/164) / [#165](https://github.com/fabiodalez-dev/Pinakes/issues/165))\n\nThe ISBN scanner now commits on **Enter** even when a partial prefix matches an existing entry, and book-cover replacement is a single step (no dead links left when an external cover can't be downloaded).\n\n### Install \u0026 operations robustness\n\n- **cPanel install fix:** when the document root is the project root (a very common shared-hosting layout), the installer now self-heals the root `.htaccess` so routing and all assets work even though cPanel's File Manager hides the dotfile during extraction. No manual step required.\n- **`/chi-siamo` (and localized CMS pages)** resolve reliably: the CMS page lookup tolerates a row seeded with a different-locale slug.\n- **Login rate limit** relaxed from 5 to **15 attempts / 5 min** — far fewer accidental lockouts during setup, still bounded against brute force.\n\n### UI polish\n\nThe book page \"Cerca su\" external-search block moved to its own row (no longer crammed in with the action buttons), genre breadcrumb separators are vertically aligned, and the related-book availability/eBook badges no longer overlap. Bundled plugin **goodlib** bumped to 1.0.1 (ships to new installs and is overwritten on upgrade).\n\n### Testing\n\nA 32-point regression suite covers the scanner/cover and backup/restore work ([#168](https://github.com/fabiodalez-dev/Pinakes/pull/168)), plus 26 new tests for the install/CMS/rate-limit/UI fixes. The full lifecycle suite (135) is green and the real admin-UI upgrade path (`reinstall-test.sh` Test B) passes; PHPStan level 5 clean. No new migration in this release (schema baseline stays at `migrate_0.7.17.sql`).\n\n---\n\n## What's New in v0.7.18\n\n### Configurable loan \u0026 reservation system (#157)\n\nThe loan lifecycle is now fully admin-configurable from **Settings → Loans**: default loan duration, maximum active loans per user (`0` = unlimited), maximum renewals, and the pickup window for approved loans. A unified, multi-copy **occupancy model** governs availability — a copy is occupied by an active loan (`in_corso` / `in_ritardo` / `da_ritirare` / `prenotato`) or by a pending request that already holds a copy, while a *bare* pending request (no copy assigned yet) does not block other users until an admin approves it and assigns a copy. Returning a copy automatically reassigns it to the next waiting reservation in the queue, deferred email notifications are flushed only **after** the transaction commits (each send isolated so one failure can't drop the others), and maintenance automations handle pickup expiry and scheduled-reservation conversion. Database changes ship in **`migrate_0.7.17.sql`** (loan settings + reworked overlap triggers, applied through a DELIMITER-aware updater step).\n\n### Private mode — restrict the site to registered users (#158)\n\nA new **Settings → Advanced → Private mode** switch makes the entire public site (home, catalog, book pages) require login. It is **off by default**. When enabled, unauthenticated API calls get a JSON `401`, private uploads are withheld, but public assets (book covers, branding) stay reachable, and the API-key-gated `/api/public/*` routes keep responding through their own `ApiKeyMiddleware` instead of being pre-empted by a session `401`.\n\n### English admin routes (#145)\n\nAll `/admin/*` paths are now English literals (`/admin/books`, `/admin/loans`, `/admin/reservations`, `/admin/users`, `/admin/publishers`, `/admin/genres`, …) instead of Italian. Old Italian admin URLs keep working through legacy redirects (`301` for `GET`, `308` for `POST` so form submissions preserve their body and CSRF), so existing bookmarks and integrations don't break. Admin routes are deliberately **not** part of the i18n route system — they are fixed English paths.\n\n### Testing\n\nValidated end to end on the merged `main`: the full lifecycle suite (135 passing), the dedicated loan / reservation / overlap suites (35 + 26 + 21), and a new private-mode suite (10), all green, with PHPStan level 5 clean.\n\n---\n\n## What's New in v0.7.16\n\n### Multi-publisher, hardened end to end (#143)\n\nBooks can have more than one publisher (the `libri_editori` junction, introduced in 0.7.15). This release closes every gap in that model: publisher **filters, counts, exports, the public publisher archive, the catalog facet, search, the admin API and bulk operations** now all match a book whether the publisher is its primary one (`libri.editore_id`) or a secondary one in the junction. Merging two publishers re-points the junction onto the survivor **before** the cascade, so no association is lost; the publisher-delete guard counts secondary links too; and CSV / LibraryThing import and bulk-enrichment now keep the junction in sync so interop exporters (OAI-PMH, BIBFRAME) never lose a publisher.\n\nEvery new junction query is **guarded for pre-migration installs** — on a database that predates the junction table the queries gracefully fall back to the primary publisher instead of erroring.\n\n### PHP 8.2 is now the floor\n\nThe installer and the in-app updater now require **PHP 8.2+**, matching `composer.json` (`^8.2`) and the generated `platform_check.php`. Previously an 8.1 host could pass preflight and then die at the Composer bootstrap.\n\n### Other fixes\n\n- **Multi-character book-case codes** (#153): the legacy single-letter UNIQUE constraint on `scaffali.lettera` is dropped, so codes like `L1`, `L2` no longer collide.\n- **Edit form**: the \"Import from ISBN\" field is pre-filled with the book's ISBN/EAN when editing.\n- A reconciliation migration heals any `libri_editori` drift left by imports written before the sync landed.\n\n### Testing\n\nThe comprehensive E2E suite grew to **132 tests**, adding a 20-test **Archives** phase (ISAD(G) CRUD, hierarchy, SQL seeding, authority records, and the JSON/XML APIs — RiC-O JSON-LD, IIIF, OAI-PMH, SRU, MARCXML/Dublin Core/EAD3/METS) and a 9-test **multi-publisher / multi-author** phase. Validated with a fresh-install + real-upgrade regression.\n\n---\n\n## What's New in v0.7.14\n\n### Installer fix: wizard no longer wedges at step 6 (Configurazione Email)\n\nHot-fix for an install-blocking bug discovered immediately after v0.7.13: every new install on a host **without a TLD** (`localhost`, an IP literal, or any intranet hostname such as `pinakes-vm`) got stuck at step 6 of the install wizard. The default `From Email` was derived from `$_SERVER['HTTP_HOST']` and accepted when the host passed `FILTER_VALIDATE_DOMAIN` — which `localhost` does. The same value was then re-validated at submit time with `FILTER_VALIDATE_EMAIL`, which is stricter (requires a TLD), so `no-reply@localhost` was silently rejected. The form posted, the controller flagged the validation failure, the same page re-rendered, and the install never progressed.\n\n`installer/steps/step6.php` now validates the host the same way `FILTER_VALIDATE_EMAIL` will: it only adopts the live `HTTP_HOST` if `no-reply@{host}` itself passes `FILTER_VALIDATE_EMAIL`. Otherwise the default falls back to `example.com` (RFC 2606 reserved, always a syntactically valid placeholder). The user can still override the field manually.\n\nVerified end-to-end with a fresh install from the v0.7.14 ZIP on localhost: the wizard now advances past step 6 with the default value untouched and reaches step 7 (Installazione Completata) cleanly.\n\nNo schema migrations. No code changes outside the installer. Existing 0.7.13 installs are unaffected and don't need to re-install.\n\n---\n\n## What's New in v0.7.13\n\n### Performance: HTTP compression + long-term cache for static assets\n\nApache (`public/.htaccess`) and the nginx example (`.nginx.conf.example`) now ship a `# === Pinakes performance block ===` that turns on gzip/brotli compression and applies `Cache-Control: public, max-age=31536000, immutable` to versioned CSS/JS/font assets. Every directive is gated by `\u003cIfModule …\u003e` (Apache) or feature-tested (nginx) so the file stays valid on hosts where the optional modules aren't loaded. Measured locally on the home page: `vendor.bundle.js` 3.5 MB → ~800 KB gzip, `main.css` 192 KB → 30 KB gzip (−84%), HTML home 471 KB → 91 KB (−81%). Asset URLs are already version-busted with `?v=X.Y.Z`, so the 1-year `immutable` lifetime is safe — every release rotates the URL automatically.\n\nFor nginx specifically, the `location ^~ /uploads/` block now adds a `Cache-Control: public, max-age=2592000` header (cover images, uploaded media) — without this explicit add_header, the prefix-priority of `^~ /uploads/` would short-circuit the regex location that previously set caching for static files, leaving uploads served with no cache headers at all. Apache wasn't affected because `mod_headers` applies `FilesMatch` globally.\n\nExisting installations upgrading via the in-admin updater pick this up through `post-install-patch.php`: an idempotent search/replace injects the same performance block into the live `.htaccess` for every install on `0.4.0`–`0.7.12`. The patch is gated by `\u003cIfModule\u003e` and uses a stable 4-line anchor (`RewriteRule ^ index.php [QSA,L]` … `# Security Headers`) verified to exist unchanged from v0.4.9.9 through v0.7.12.\n\n### Bulk \"Scarica copertine\" self-heals missing covers ([visible bug](https://github.com/fabiodalez-dev/Pinakes/pull/144))\n\n`LibriController::fetchCover()` and `syncCovers()` used to trust `libri.copertina_url` alone when deciding whether a book already had a cover, returning `reason: already_has_cover` even when the file behind that URL had been deleted on disk (a common state after manual cleanups, partial backup restores, or failed downloads). The bulk \"Scarica copertine\" action would then report `Completato. Già presenti: 1` and the book stayed permanently uncovered.\n\nBoth methods now resolve the path with `realpath()` against `getCoversUploadPath()` and require the resolved file to live inside the covers directory (`str_starts_with($resolved, $baseDir . DIRECTORY_SEPARATOR)`) — a defence-in-depth tightening compared to the existing delete path. If the file is missing or unreachable, the controller logs a warning (`cover_url in DB but file missing/unreachable on disk, re-fetching`), re-runs the scrape, downloads a fresh cover, and updates the DB. Idempotent on subsequent calls.\n\n### Minor UI fixes\n\n- `.search-book-year` in the hero search dropdown is now explicitly left-aligned, matching the sibling `.search-book-author` line.\n- `.description-content .prose` in the book-detail page gets `max-w-none` so the description fills its column instead of being capped at Tailwind Typography's default 65ch (already constrained by the page grid).\n\n### Notes\n\n- No schema migrations — drop-in upgrade from `v0.7.12`.\n- No new bundled plugins, no breaking changes.\n- The companion `post-install-patch.php` is attached to the GitHub release and applied automatically by the in-admin updater; nothing for end users to do manually.\n\n---\n\n## What's New in v0.7.12\n\n### Archives: RiC-CM Phases 5 \u0026 6 — admin UI + OAI-PMH `ric-o` ([#122](https://github.com/fabiodalez-dev/Pinakes/issues/122))\n\nv0.7.12 closes the six-phase RiC-CM roadmap. Phases 1-4 (shipped progressively in 0.7.7 → 0.7.10) modelled the four RiC-CM entity types (Record/RecordSet, Agent, Activity, Place) and the polymorphic relations graph. Phases 5 and 6 expose them to curators and to harvesters.\n\n**Phase 5 — native admin UI for activities, places and relations.**\n\n- `GET/POST /admin/archives/activities` + `/new` + `/{id}` + `/{id}/edit` + `/{id}/delete` — CRUD over ISDF activities (Function/Activity/Transaction/Task/Mandate). Hierarchical parent/child with cycle detection on the application layer (the `parent_id` FK uses `ON DELETE SET NULL`, which is incompatible with a MySQL `CHECK` constraint, so the cycle guard is enforced in PHP before INSERT/UPDATE).\n- `GET/POST /admin/archives/places` + `/new` + `/{id}` + `/{id}/edit` + `/{id}/delete` — CRUD over places (country/region/province/municipality/locality/building/room/geographic_feature/other) with optional latitude/longitude and GeoNames / Wikidata / Getty TGN identifiers for Linked Data linkage.\n- `POST /admin/archives/relations/attach` + `POST /admin/archives/relations/{id}/detach` — manage the polymorphic relations graph from the unit/agent/activity/place detail pages.\n- `GET /api/archives/entities?type=\u0026q=` — typeahead backend for Choices.js-style autocomplete in the relation forms. Returns the four entity types validated against the ENUM definitions of `archive_relations.source_type` / `target_type`.\n\nThe chrome mirrors the existing books/archives admin views (Tailwind `p-6 max-w-4xl mx-auto`, `bg-white shadow rounded-lg p-6 space-y-5` form containers, `form-label` field labels, breadcrumb navigation, indigo-600 primary actions, red-50/red-700 destructive buttons). All 60+ user-facing strings are Italian-source `__()` wrappers with full translations added to `locale/en_US.json`, `locale/fr_FR.json`, `locale/de_DE.json`.\n\n**Phase 6 — OAI-PMH `metadataPrefix=ric-o`.**\n\n- The `oai-pmh-server` plugin now exposes `ric-o` (canonical RDF/XML serialisation of the same RiC-O graph emitted on `/archives/{id}/ric.json`) as a metadataPrefix for the `archives` set. `ListMetadataFormats` advertises it conditionally — only when the `archives` plugin is active AND the `archival_units` table exists.\n- `GetRecord?identifier=oai:…:archival_unit:{id}\u0026metadataPrefix=ric-o` serialises one archival unit as `\u003crdf:RDF\u003e` with `ric:RecordSet` / `ric:Record` root, `rdfs:label` carrying `xml:lang`, `ric:DateRange` with `xsd:gYear` typed literals, embedded `ric:Relation` subjects for agent links, and `rdf:resource` references for parent/children. `ListRecords?set=archives\u0026metadataPrefix=ric-o` streams the whole archival graph.\n- Symmetric validation: `metadataPrefix=ric-o` on `set=books` or on a book identifier returns `cannotDisseminateFormat`; `metadataPrefix=oai_dc` keeps working on both sets unchanged.\n- Re-uses `RicJsonLdBuilder::serializeToRdfXml()` (new in this release) which translates the JSON-LD compact document to canonical RDF/XML — `@id`→`rdf:about`/`rdf:resource`, `@type`→tag name (CURIE expanded against the document `@context`), language tags via `xml:lang`, typed literals via `rdf:datatype`, nested blank nodes for inline objects. 159/159 unit assertions passing on the round-trip.\n\nThe full RiC-CM journey: v0.7.7 read-only JSON-LD → v0.7.8 agents → v0.7.9 activities → v0.7.10 places + polymorphic relations → v0.7.12 admin UI + OAI-PMH RDF/XML. The application's `version.json` bumps from 0.7.10 to 0.7.12 once, at the end of the chain.\n\n**Cleanup — dead schema column dropped (review F015).** The `archive_activities.place_id` column was introduced in 0.7.9 as a placeholder reserved for the Phase 4 `archive_places` FK, but Phase 4 (0.7.10) chose the polymorphic `archive_relations` graph instead and no application code ever read or wrote the column. Migration `migrate_0.7.12.sql` drops it with `ALTER TABLE archive_activities DROP COLUMN place_id;` so the schema reflects what the code actually uses.\n\n---\n\n## What's New in v0.7.10\n\n### Archives: RiC-CM Phase 4 — Places + polymorphic Relations graph ([#122](https://github.com/fabiodalez-dev/Pinakes/issues/122))\n\nFourth phase of the RiC-CM roadmap. With Phases 1-3 we modelled three of the five RiC-CM entity types (Record/RecordSet, Agent, Activity). Phase 4 introduces the fourth — **Place** — and the **generic polymorphic Relations** backbone that lets any pair of entities carry a typed RiC-O predicate. The model is now complete on the entity side.\n\n- **New table `archive_places`** — first-class Place entity (RiC-CM §3.5). `name` + `place_type` ENUM (country / region / province / municipality / locality / building / room / geographic_feature / other), self-referential `parent_id` for the place hierarchy (Catania → Sicilia → Italia), optional `latitude` / `longitude` for map display, optional `geonames_id` / `wikidata_id` / `tgn_id` for external Linked Data identifiers, optional `date_start` / `date_end` for historical places (e.g. \"Regno delle Due Sicilie\", 1816-1861). Full-text index on `name + description`.\n- **New table `archive_relations`** — **polymorphic** N:M relations between any two RiC-CM entities. Both endpoints (`source_type`+`source_id` and `target_type`+`target_id`) reference one of four entity types: `archival_unit`, `authority_record`, `archive_activity`, `archive_place`. The `ric_predicate` column is VARCHAR so RiC-O's open vocabulary can grow without migrations. Common predicates: `ric:isOrWasLocatedAt`, `ric:isOrWasResidentAt`, `ric:isOrWasPerformedAt`, `ric:isOrWasIncludedIn`. Each row carries optional `qualifier`, `certainty` (certain/probable/uncertain), `date_start`/`date_end` for temporal validity, `source_ref` for the documentary citation, and `created_by` to track curatorial provenance.\n- **Why polymorphic, not 16 specialised link tables** — RiC-O has dozens of inter-entity predicates. One link table per (source, target, predicate) triple would explode the schema and add a migration on every new predicate. Polymorphic source/target keeps the schema compact; the application-layer validator (`validateRelationEndpoints`) checks both endpoints exist and are not soft-deleted before INSERT.\n- **Two new public endpoints**:\n  - `GET /archives/places/{id}/ric.json` — RiC-O JSON-LD for one place. Emits `ric:Place`, `ric:CoordinateLocation` from lat/lng, `owl:sameAs` to GeoNames / Wikidata / Getty TGN, `ric:isOrWasIncludedIn` to the parent place, and `ric:isAssociatedWithDate` for historical date ranges.\n  - `GET /archives/places/ric.json` — synthetic `ric:RecordSet` listing every top-level place (those with `parent_id IS NULL`), suitable for harvesting alongside the existing collection / agents / activities endpoints.\n- **`RicJsonLdBuilder::buildRelationNode()`** — new method that renders any `archive_relations` row as a `ric:Relation` JSON-LD node with deterministic `@id` (`/archives/relations/{row.id}`), `ric:relationHasSource` and `ric:relationHasTarget` resolved via the central `iriForEntity()` switch. Returns `null` on malformed input — no exception — so callers can drop bad rows from the output without crashing the whole response.\n- **`validateRelationEndpoints(sourceType, sourceId, targetType, targetId)`** — application-layer integrity check used by the admin form before inserting into `archive_relations`. Verifies both endpoints exist and are not soft-deleted; the polymorphic column shape makes a SQL FK impossible.\n- **Migration `migrate_0.7.10.sql`** — idempotent. `archive_places.parent_id` self-cycle guards live in the application layer (MySQL forbids CHECK on a column that's part of an `ON DELETE SET NULL` FK action, same constraint encountered in Phase 3).\n\n## What's New in v0.7.9\n\n### Archives: RiC-CM Phase 3 — Activities as first-class entities ([#122](https://github.com/fabiodalez-dev/Pinakes/issues/122))\n\nThird milestone of the RiC-CM roadmap. Introduces the ISDF-aligned **Activity** entity — any human or organisational activity that produced, used, or managed archival material. Phase 1 + Phase 2 already gave us records, record sets, and agents; Phase 3 closes the \"what happened\" side of the RiC-CM triangle.\n\n- **New table `archive_activities`** — first-class Activity entity. Columns: `title`, `description`, `activity_type` (`function` / `activity` / `transaction` / `task` / `mandate` per ISDF terminology), self-referential `parent_id` (so a function can contain activities, an activity can contain transactions), `date_start` / `date_end` / `is_ongoing`, optional `agent_id` FK to `authority_records` (the agent that performed the activity), `place_id` reserved for Phase 4, `source_ref` for the legal/normative citation (e.g. \"RD 9 ottobre 1861 n. 250\"), full-text index on title + description.\n- **New table `archive_unit_activities`** — M:N link between archival units and activities. The `ric_predicate` column captures the semantics of each link as a RiC-O predicate: `ric:resultsOrResultedFrom` (the unit was produced by the activity, default), `ric:isOrWasUsedBy` (the unit was used during the activity), `ric:isSubjectOf` (the activity is about this unit). Column is VARCHAR so new predicates can be added without a migration.\n- **Two new public endpoints**:\n  - `GET /archives/activities/{id}/ric.json` — RiC-O JSON-LD for one activity, with `ric:Activity` type, `ric:isOrWasPerformedBy` → agent, `ric:hasOrHadPartOf` → parent activity, `ric:isAssociatedWithDate` as `ric:DateRange` (`xsd:date`), and `ric:isOrWasRelatedTo` listing every unit the activity produced / used.\n  - `GET /archives/activities/ric.json` — synthetic `ric:RecordSet` listing every top-level activity (those with `parent_id IS NULL`), suitable for ICA / Europeana harvesting alongside the existing collection.ric.json and agents endpoints.\n- **`/archives/{id}/ric.json` now embeds activity links** — `RicJsonLdBuilder::buildUnit()` accepts a new `$activities` parameter so the unit-side serialisation lists every activity it's connected to. The relation IRI is shared between the unit side and the activity side (`/archives/unit-activity-relations/{unitId}-{activityId}-{predicate-slug}`) so a graph-merge consumer collapses both emissions into a single RDF node.\n- **Migration `migrate_0.7.9.sql`** — idempotent. The CHECK constraint guarding `parent_id \u003c\u003e id` is intentionally absent because MySQL rejects CHECK on a column that's part of a FK referential action (`ON DELETE SET NULL`); the application-layer cycle guard in `activityWouldCreateCycle()` provides the equivalent protection.\n\n## What's New in v0.7.8\n\n### Archives: RiC-CM Phase 2 — Agents as first-class entities ([#122](https://github.com/fabiodalez-dev/Pinakes/issues/122))\n\nPhase 2 of the 6-phase RiC-CM roadmap. Phase 1 (v0.7.7) was schema-free; this is the first migration in the chain that touches the DB.\n\n- **`authority_records` extended** — four new columns:\n  - `ric_type` (`ENUM('Person','CorporateBody','Family','Position','Group')`) — RiC-CM canonical type, broader than the legacy ISAAR `type` enum. The migration backfills it from existing `type` values; `Position` and `Group` are RiC-CM-only types ISAAR doesn't model.\n  - `birth_date` / `death_date` — structured begin/end-of-existence dates (`xsd:date`). The RiC-O JSON-LD output now emits `ric:beginningDate` and `ric:endDate` as typed literals instead of the free-text `dates_of_existence` blob (which is preserved for back-compat and surfaces as `ric:descriptiveNote` on pre-Phase-2 rows).\n  - `place_of_origin` — birthplace / founding place. Phase 4 will swap this literal for a FK to a dedicated `archive_places` table.\n- **New table `archive_agent_identifiers`** — multi-scheme identifier ledger for archive authorities (VIAF, ISNI, Wikidata, GND, BNF, LCNAF, Getty ULAN, ARK, local). Each row carries scheme + value + optional precomputed URI + an `is_preferred` flag. `collectSameAsForAuthority` now merges these into `owl:sameAs` alongside the existing `viaf-authority` plugin's data; rows without a precomputed URI are synthesised from the scheme's canonical prefix (e.g. `viaf:29539` → `https://viaf.org/viaf/29539`).\n- **New table `archive_agent_relations`** — Agent ↔ Agent edges typed with a RiC-O predicate (`ric:isParentOf`, `ric:isMemberOf`, `ric:isSuccessorOf`, `ric:isMarriedTo`, ...). Captures organisational hierarchies, corporate successions, and family ties that ISAAR's flat table cannot express. Each row becomes a `ric:Relation` node in the RiC-O JSON-LD output with a deterministic `@id` of the form `{base}/archives/agent-relations/{agentId}-{relatedId}-{predicate-slug}`. The schema rejects self-loops via a `CHECK` constraint (MySQL 8.0.16+).\n- **Migration `migrate_0.7.8.sql`** — fully idempotent (INFORMATION_SCHEMA guards on every ALTER, `CREATE TABLE IF NOT EXISTS` on every CREATE). Re-running the migration is safe; the ric_type backfill UPDATE narrows on rows still at the default value so curator overrides survive.\n\n## What's New in v0.7.7\n\n### Regression hotfix for author autocomplete ([#74](https://github.com/fabiodalez-dev/Pinakes/issues/74))\n\n- **Issue #74 regression fix** — typing a new author name in the book form and pressing Enter was once again selecting the first highlighted dropdown match (e.g. typing \"Norbert Wex\" picked the existing \"Norbert Bauer\") instead of creating the new author. The original fix in v0.4.9.4 monkey-patched `authorsChoice._onEnterKey` on the Choices.js instance; a later \"cleaner\" refactor (commit `e976cb1e`) replaced it with a capture-phase keydown listener, which Choices.js v11 silently bypasses via `stopImmediatePropagation()` on its own pre-registered capture-phase handler. Restored the monkey-patch with a defensive capture-phase fallback for any future Choices.js version that removes `_onEnterKey`. The override is per-instance, so publisher / genre / etc. Choices instances on the same page keep stock behaviour.\n\nThis is a patch-only release. No schema migrations, no plugin changes,\nno config changes required. Drop-in upgrade from v0.7.6.\n\n---\n\n## What's New in v0.7.6\n\n### French locale (fr_FR) and BNF scraping\n\n- **Full French translation** — 4,145 translated keys (100% coverage). Select `fr_FR` during the installation wizard to run Pinakes in French; existing installations can switch the default locale from Settings → Localisation.\n- **BNF SRU scraping** — the Z39 Server plugin now connects to the Bibliothèque nationale de France SRU endpoint and maps UNIMARC fields to Pinakes metadata (title, authors, publisher, ISBN, Dewey, subjects). Enable the Z39 Server plugin and add `sru.bnf.fr` as a source to start importing French bibliographic records.\n- **Migration hardening** — `migrate_0.7.5.sql` now uses `ON DUPLICATE KEY UPDATE` instead of `INSERT IGNORE`, ensuring `fr_FR` is correctly re-activated on upgrades where the language row already existed with `is_active=0`. `Language::setDefault()` now forces `is_active=1` on the target language to prevent an inconsistent state where the default locale is invisible to the resolution chain.\n- **Dev-schema guard** — `migrate_0.7.0.sql` detects installations where `author_authority_alternates` was created with the legacy column name `source_code` and automatically drops and recreates the table, preventing a fatal `ADD KEY` error during upgrade.\n\n### Archives: IIIF Presentation 3.0 and AtoM alignment ([#123](https://github.com/fabiodalez-dev/Pinakes/issues/123), [#121](https://github.com/fabiodalez-dev/Pinakes/issues/121))\n\n- **IIIF Presentation 3.0 manifests** — `GET /archives/{id}/manifest.json` returns a standards-compliant IIIF 3.0 manifest for each archival unit, exposing attached digitised documents as `Canvas` items with painting `Annotation`s. Works out of the box with Universal Viewer, Mirador, and other IIIF viewers.\n- **AtoM ISAD(G) area labels** — the Archives admin UI and public display now use canonical ISAD(G) area names (`Identity area`, `Context area`, `Content and structure area`, `Conditions of access and use area`, `Allied materials area`, `Notes area`) so records are immediately recognisable to users coming from AtoM or other archival systems.\n- **Multi-document uploads** — archival units now support multiple attached digitised files (PDF, JPEG, TIFF). Each file is stored with its original name, MIME type, and display order.\n\n### Security fixes\n\n- **Open-redirect via Host spoofing** — the OpenURL resolver built redirect URLs directly from `$request-\u003egetUri()-\u003egetHost()`, bypassing the `APP_TRUSTED_HOSTS` guard in `HtmlHelper::getBaseUrl()`. A crafted `Host:` header could redirect users to an attacker-controlled domain. Fixed to use `absoluteUrl()`.\n- **CQL injection in SRU client** — search terms containing `\"` or `\\` were embedded in CQL quoted-term syntax without escaping, producing malformed queries sent to external SRU endpoints (BNF, SUDOC). Fixed with proper backslash escaping per the CQL specification.\n\n### Compatibility fixes\n\n- **Windows updater** ([#130](https://github.com/fabiodalez-dev/Pinakes/issues/130)) — path separators are now normalised to forward slashes before version-file lookups; backslash paths on Windows caused the updater to silently fail.\n- **German routes** — added the missing `bibframe.book` route key to `routes_de_DE.json`, bringing German routing to parity with Italian, English, and French.\n\n---\n\n## What's New in v0.7.4\n\n\u003e Releases v0.6.x through v0.7.4 focused on library interoperability and archive search. All changes are listed below newest-first.\n\n### Archive search bar — admin + public (v0.7.4)\n\nThe **Archives** plugin now ships a full search interface on both the admin and the public catalog.\n\n**Admin (`/admin/archives?q=…\u0026level=…`)**\n- Free-text search hits `reference_code` (LIKE, for short codes like `IT-MI-001`), `constructed_title`/`formal_title` (LIKE), and `scope_content`/`archival_history` (MySQL FULLTEXT — two-pass query, deduplicated).\n- Level filter (`fonds` / `series` / `file` / `item`) narrows by archival hierarchy without a separate page.\n- Search mode renders a flat list instead of the tree indent, making all matched nodes equally scannable regardless of depth.\n- Result counter (`N risultati per \"query\" · livello: series`) and input persistence (query + selected level remain filled after submission).\n- \"Azzera\" reset link returns to the full hierarchical tree.\n\n**Public (`/archivio?q=…\u0026level=…\u0026date_from=…\u0026date_to=…`)**\n- Same text + level filters plus a **date range** filter: `date_from` matches units whose `date_end ≥ year`; `date_to` matches units whose `date_start ≤ year`; both can be combined for an overlap query.\n- In search mode results include all hierarchy levels (series, files, items), not just root fonds — so a reference-code search for `IT-MI-ARC-001/2` finds the exact fascicolo.\n- Theme-aware CSS (`.archive-search-form`) reads `--primary-color` / `--archives-color-primary` so the form inherits whatever palette the admin chose in Settings → Appearance.\n- × reset button clears all filters back to the root catalog.\n\n**Bug fixes included**\n- `reference_code` was previously not searchable at all — the old endpoint used only FULLTEXT, which skips tokens shorter than `ft_min_word_len` (3); the new two-pass strategy uses LIKE first.\n- Level filter was silently ignored due to a PHP associative-array bug (`in_array` was checking integer values `[1,2,3,4]` instead of string keys `['fonds','series',…]`); corrected to `isset(self::LEVELS[$level])`.\n\n**E2E coverage**: `tests/archives-search.spec.js` — 25 serial tests covering admin search (15) and public search (10), run with `/tmp/run-e2e.sh tests/archives-search.spec.js --config=tests/playwright.config.js --workers=1`.\n\n### Interoperability stack — OAI-PMH, NCIP, BIBFRAME, ResourceSync, OpenURL, VIAF (v0.7.x)\n\nPinakes v0.7.x introduced a full library-interoperability layer, delivered as opt-in plugins that activate without touching the core schema.\n\n**OAI-PMH 2.0 data provider** (`/archives/oai`)\n- Exposes archival units as OAI-PMH records. Supports `Identify`, `ListMetadataFormats` (`oai_dc`, `marc21`), `ListSets` (one set per ISAD level), `ListRecords`, `GetRecord`, and resumption-token-based pagination.\n- Dublin Core crosswalk from ISAD fields (title, description, date, identifier, type); MARCXML crosswalk from the same ABA field mapping used by the SRU endpoint.\n- Selective harvesting by set (`level:fonds`, `level:series`, …) and by `from`/`until` date range (uses `updated_at`).\n\n**NCIP 2.02 server**\n- Implements the NISO Circulation Interchange Protocol: `LookupUser`, `LookupItem`, `CheckOutItem`, `CheckInItem`, `RenewItem`, `RequestItem`, `CancelRequestItem`.\n- Partner library management UI at `/admin/plugins/ncip-server/partners` and `/admin/plugins/ncip-server/transactions` — register external systems with shared secret, set borrowing quotas.\n- Maps Pinakes loan/reservation/user records onto NCIP data elements; returns structured NCIP XML responses.\n\n**BIBFRAME 2.0 linked-data export**\n- `GET /api/bibframe/book/{id}` — emits JSON-LD `bf:Work` + `bf:Instance` for books.\n- `GET /api/bibframe/book/{id}/work` — `bf:Work` only.\n- `GET /api/bibframe/book/{id}/instance` — `bf:Instance` only.\n- Includes `bf:title`, `bf:contribution` (authors as `bf:Agent`), `bf:subject` (keywords), `bf:genreForm`, `bf:classification` (Dewey), `bf:language`, `bf:identifiedBy` (ISBN-13, EAN), and persistent `/id/work/{id}` + `/id/instance/{id}` URIs.\n\n**ResourceSync**\n- `GET /.well-known/resourcesync` — W3C ResourceSync source description.\n- `GET /resync/capabilitylist.xml` — capability list linking to resource list and change list.\n- `GET /resync/resourcelist.xml` — enumeration of all book and archive URLs with `md:hash` (MD5) and `md:lastmod`.\n- `GET /resync/changelist.xml` — incremental change log (created/updated/deleted) since a given `from` date.\n\n**OpenURL / COinS**\n- OpenURL 1.0 resolver at `/openurl` — parses `ctx_ver=Z39.88-2004` + `rft.*` parameters, resolves to full-text link, catalog record, or ILL form.\n- COinS `\u003cspan class=\"Z3988\"\u003e` auto-embedded in public book detail pages for Zotero/Mendeley browser extensions.\n\n**VIAF auto-linking**\n- Scheduled task checks unlinked `authority_records` against the VIAF SRU endpoint; fills `viaf_id` for exact-name matches.\n- Admin UI at `/admin/archives/authorities` shows VIAF reconciliation status per record and allows manual override.\n\n**Documentation**: full technical guides (IT + EN) published at \u003chttps://fabiodalez-dev.github.io/Pinakes/\u003e — one page per protocol.\n\n### Membership consistency hardening + performance indexes (v0.5.9.6)\n\n- `libri_collane` now enforces a CHECK constraint (`chk_lc_principale_consistency`) so a row can never have `tipo_appartenenza='principale'` together with `is_principale=0` (or vice versa). Pre-fix the column defaults silently allowed that contradictory state.\n- The column default for `is_principale` was aligned to `1` to match the `'principale'` default of `tipo_appartenenza`, removing the foot-gun for any future plugin/CSV/scraper that omits the flag.\n- Existing rows are realigned in-place by an idempotent migration; no data loss, no manual steps required.\n- Six performance indexes backfilled for existing installations via `migrate_0.5.9.6.sql`: `idx_origine` and `idx_libro_utente` on `prestiti`; `idx_tipo_utente` on `utenti`; `idx_stato_libro`, `idx_queue_position` on `prenotazioni`. Fresh installs already had these via `schema.sql`; upgrades from any prior version now receive them automatically.\n\n### Series groups and cycles (v0.5.9.5)\n\n- Collane now support an optional umbrella group for related spin-offs, universes, or franchises, so separate series like `Fairy Tail`, `Fairy Tail: 100 Year Quest`, and `Fairy Tail: Happy` can remain distinct while sharing one parent group.\n- Collane also support an optional cycle/season label plus numeric ordering, matching LibraryThing-style series such as `The Worlds of Aldebaran` with `Cycle 1`, `Cycle 2`, and later arcs.\n- Book create/edit forms can set group, cycle/season, cycle order, series name, and series number in one flow; the Collane admin page exposes the same metadata and shows related series in the same group.\n\n### Archives plugin (ISAD(G) / ISAAR(CPF))\n\nNew bundled plugin for archival material alongside the bibliographic\ncatalog — hierarchical descriptions (Fondo → Series → File → Item) per\n[ISAD(G)](https://www.ica.org/en/isadg-general-international-standard-archival-description-second-edition),\nauthority records per\n[ISAAR(CPF)](https://www.ica.org/en/isaar-cpf-international-standard-archival-authority-record-corporate-bodies-persons-and-families-2nd).\n\n**Archival descriptions**\n\n- Three tables (`archival_units`, `authority_records`, `archival_unit_authority`)\n  with self-referencing tree, FK guards, MARC-like field crosswalk inspired\n  by the ABA format (Arbejderbevægelsens Bibliotek og Arkiv).\n- Admin CRUD at `/admin/archives`, public frontend at `/archivio` (card grid\n  + detail pages styled to match the book detail, SEO slug URLs, JSON-LD\n  `ArchiveComponent` schema, breadcrumb chain).\n- Per-unit cover image + document uploads (PDF/ePub/MP3/video) with finfo\n  MIME detection and path-prefix unlink guard.\n\n**Authority records (ISAAR(CPF))**\n\n- Full CRUD for persons / corporate bodies / families with M:N linkage\n  to both `archival_units` and `libri.autori` (unified authority file\n  for the whole catalog, not per-module).\n- JS type-ahead picker for attaching an existing authority to an\n  archival unit (admin form) — no manual ID entry.\n- Unified cross-entity search: a single query returns hits across\n  `libri` + `archival_units` + `authority_records` with the correct\n  provenance label in the results.\n\n**Photographic items**\n\n- Dedicated `specific_material` ENUM on `archival_units` covering the\n  full ABA billedmarc / MARC21 008-pos-33 catalogue (`hb`/`hp`/`hm`/`hd`/`hk`/\n  `bf`/`hf`/`lm`/`lf`/`vm`/`bm`/`le`/`zz`…) so a photograph, postcard,\n  drawing, map, or audio-visual item gets classified correctly rather\n  than flattened to \"item\".\n\n**MARCXML I/O + SRU**\n\n- MARCXML import + export, round-trip-stable (identity test: export →\n  import → re-export yields byte-identical output), validated against\n  the MARC21 Slim XSD on both sides.\n- SRU 1.2 endpoint for archival records so external discovery systems\n  (OPACs, union catalogues, Z39.50/SRU gateways) can query the archive\n  alongside the book catalogue.\n\n**Packaging**\n\n- Plugin ships **inactive** (`metadata.optional: true`). Activate in\n  Admin → Plugins to create the schema.\n- i18n: IT/EN/DE (~40 new keys). Tracks\n  [#103](https://github.com/fabiodalez-dev/Pinakes/issues/103).\n\n### Discogs catalog number (Cat#) support\n\n`DiscogsPlugin::validateBarcode` now accepts Catalog Numbers\n(`CDP 7912682`, `SRX-6272`, `DGC-24425-2`) alongside EAN-13/UPC-A.\n`ScrapeController::byIsbn` preserves the raw identifier through the\n`scrape.isbn.validate` hook chain so plugins can match non-numeric\ninputs. Valid ISBN-10 codes ending in `X` (`080442957X`) are explicitly\nvetoed from Cat# classification to avoid music-metadata merges into book\nrecords (MOD-11 checksum in `DiscogsPlugin::isIsbn10`, 7 regression\nasserts in `tests/discogs-catno.unit.php`). Closes\n[#101](https://github.com/fabiodalez-dev/Pinakes/issues/101).\n\n### Remember-me preserves user locale\n\nUsers whose `utenti.locale` differs from the install default\n(a `de_DE` user on an `it_IT` install) now see their locale restored\nafter auto-login. Fix is in installer seed + a backfill migration:\n`installer/database/data_{it_IT,en_US}.sql` seed all three shipped\nlocales, `migrate_0.5.9.1.sql` adds the missing row on existing\ninstalls. Closes [#108](https://github.com/fabiodalez-dev/Pinakes/issues/108).\n\n### Migration\n\n`migrate_0.5.9.sql` creates archival plugin tables + indexes.\n`migrate_0.5.9.1.sql` seeds missing locales. Both idempotent via\n`INFORMATION_SCHEMA` guards and `INSERT IGNORE`.\n\n### Release-pipeline hardening (v0.5.9.2 → v0.5.9.4)\n\nThe 0.5.9.x series took four hotfix iterations because a forgotten\nGitHub Actions workflow (`release.yml`) was racing\n`scripts/create-release.sh` and overwriting the published ZIP with a\nstale build that only contained 5 of 10 bundled plugins. The rogue\nworkflow is now disabled, `bin/build-release.sh` enumerates plugins\nfrom the filesystem instead of a hardcoded list, and\n`scripts/create-release.sh` verifies the shipped ZIP via the GitHub\nAPI (uploader identity + SHA + plugin count, polled for 90s) so no\nthird-party overwrite can slip through unnoticed. Full post-mortem\nin `updater.md`.\n\n---\n\n## Previous Releases\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003ev0.5.4\u003c/strong\u003e - Discogs Plugin + Media Type + Plugin Manager Hardening\u003c/summary\u003e\n\n### Discogs music scraper plugin (#87)\n\n- **New `tipo_media` ENUM** (`libro/disco/audiolibro/dvd/altro`) on `libri` with composite index `(deleted_at, tipo_media)`\n- **Heuristic backfill** from `formato` using anchored LIKE patterns (avoids `%cd%` matching CD-ROM, `%lp%` matching \"help\")\n- **Discogs + MusicBrainz + CoverArtArchive + Deezer** chain with 4 hooks (incl. `scrape.isbn.validate` for UPC-12/13)\n- **Barcode → ISBN guard** in `ScrapeController::normalizeIsbnFields` — skips normalization when no format/tipo_media signal to avoid the EAN-in-`isbn13` regression\n- **PluginManager** migrated from `error_log` → `SecureLogger` (31 call sites)\n\n### Post-release hotfixes (rolled into v0.5.4)\n\n- `autoRegisterBundledPlugins` INSERT had 14 columns / 13 values after CodeRabbit round 11 — fresh installs crashed with \"Column count doesn't match value count\" (fixed in `c9bd82c`)\n- Same method's `bind_param('ssssssssissss')` had positions 8+9 swapped — `path='discogs'` was cast to int `0`, orphan-detection then deleted the rows (fixed in `fb1e881`)\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003ev0.5.3\u003c/strong\u003e - Cross-Version Consistency Fixes (v0.4.9.9–v0.5.2)\u003c/summary\u003e\n\n- **`descrizione_plain` propagated** — Catalog FULLTEXT search and admin grid now use `COALESCE(NULLIF(descrizione_plain, ''), descrizione)` for LIKE conditions, completing the HTML-free search feature from v0.4.9.9\n- **ISSN in Schema.org \u0026 API** — `issn` property now emitted in Book JSON-LD and returned by the public API (`/api/books`)\n- **CollaneController atomicity** — `rename()` aborts on `prepare()` failure instead of committing partial state\n- **LibraryThing import aligned** — `descrizione_plain` (with `html_entity_decode` + spacing), ISSN normalization, `AuthorNormalizer` on traduttore, soft-delete guards on all UPDATE queries, and `descrizione_plain` column conditional (safe on pre-0.4.9.9 databases)\n- **Secondary Author Roles** — LT import now routes translators to `traduttore` field based on `Secondary Author Roles`\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003ev0.5.2\u003c/strong\u003e - Name Normalization (#93)\u003c/summary\u003e\n\n### Name Normalization for Translators, Illustrators, Curators (#93)\n\n- **`AuthorNormalizer`** applied to translator, illustrator, and curator on create, update, and scraping\n- **Client-side normalization** — \"Surname, Name\" → \"Name Surname\" for translator/illustrator in book form\n- **Shared `normalizeAuthorName()`** JS helper across authors, translator, illustrator\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003ev0.5.1\u003c/strong\u003e - ISSN, Series Management, Multi-Volume Works (#75)\u003c/summary\u003e\n\n### ISSN, Series Management, Multi-Volume Works (#75)\n\n**ISSN Field:**\n- **New ISSN field** on book form with XXXX-XXXX validation (server-side + client-side)\n- **Displayed on frontend** book detail and in public API responses\n- **Schema.org** `issn` property emitted in JSON-LD\n\n**Series (Collane) Management:**\n- **Admin page** `/admin/collane` — List all series with book counts, create, rename, merge, delete\n- **Series detail** page — Description editor, book list with volume numbers, autocomplete merge\n- **Bulk assign** — Select multiple books and assign to a series from the book list\n- **Search autocomplete** — Series name suggestions in merge and bulk assign dialogs\n- **Empty series preserved** — Series with no books still appear in the admin list\n- **Frontend \"Same series\"** section — Book detail shows other books in the same series\n\n**Multi-Volume Works:**\n- **`volumi` table** — Links parent works to individual volumes with volume numbers\n- **Admin UI** — Add/remove volumes via search modal, volume table on book detail\n- **Parent work badge** — \"This book is volume X of Work Y\" badge with link\n- **Cycle prevention** — Full ancestor-chain walk prevents circular relationships\n- **Create from collana** — One-click creation of parent work from a series page\n\n**Import Improvements:**\n- **LibraryThing Series parsing** — Splits `\"Series Name ; Number\"` into separate collana + numero_serie\n- **Scraping series split** — Same parsing for ISBN scraping results\n- **CSV/TSV import** — `collana` field already supported with multilingual aliases\n\n**Bug Fixes \u0026 Improvements:**\n- **ISSN validation** — Explicit error message instead of silent discard\n- **Transactions** — Delete, rename, merge collane wrapped in DB transactions\n- **Error handling** — execute() results checked in all AJAX endpoints\n- **Soft-delete guards** — addVolume rejects deleted books, updateOptionals includes guard\n- **Migration resilience** — `hasCollaneTable()` guard for partial migration scenarios\n- **Non-numeric volume sorting** — Special volumes sort after numbered ones\n- **Unified search fix** — Add-volume modal correctly parses flat array response\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003ev0.5.0\u003c/strong\u003e - SEO \u0026 LLM Readiness, Schema.org Enrichment, Curator Field\u003c/summary\u003e\n\n### SEO \u0026 LLM Readiness, Schema.org Enrichment, Curator Field\n\n- **Hreflang alternate tags** on all frontend pages\n- **RSS 2.0 feed** at `/feed.xml`\n- **Dynamic `/llms.txt`** endpoint (admin-toggleable)\n- **Schema.org enrichment** — Book `sameAs`, all author roles, `bookEdition`, conditional `Offer`\n- **New `curatore` field** — Database, form, admin detail, Schema.org `editor`\n- **CSV column shift fix (#83)**, admin genre display fix (#90)\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003ev0.4.9.9\u003c/strong\u003e - Social Sharing, Genre Navigation, Search Improvements\u003c/summary\u003e\n\n### Social Sharing, Genre Navigation, Inline PDF Viewer \u0026 Search\n\n- **7 sharing providers** — Facebook, X, WhatsApp, Telegram, LinkedIn, Reddit, Pinterest + Email, Copy Link, Web Share API\n- **Genre breadcrumb navigation** — Clickable genre hierarchy links that filter by category\n- **Inline PDF viewer** — Browser-native `\u003ciframe\u003e` PDF viewer (Digital Library plugin v1.3.0)\n- **Description-inclusive search** — New `descrizione_plain` column for HTML-free search\n- **RSS icon in footer** — SVG feed icon next to \"Powered by Pinakes\"\n- **Auto-hook registration** — Plugin hooks auto-registered on page load\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003ev0.4.9.8\u003c/strong\u003e - Security, Database Integrity \u0026 Code Quality\u003c/summary\u003e\n\n### Security \u0026 Database Integrity\n\n- **SMTP password encryption** — AES-256-CBC at rest using `APP_KEY`\n- **isbn10/ean UNIQUE indexes** — Blank values normalized to NULL, duplicates resolved\n- **prestiti FK fix** — Foreign key corrected to reference `utenti(id)`\n- **Email notification test suite** — 16 Playwright E2E tests covering all email types\n\n\u003c/details\u003e\n\n---\n\n## Quick Start\n\n1. **Clone or download** this repository and upload all files to the root directory of your server.\n2. **Visit your site's root URL** in the browser — the guided installer starts automatically.\n3. **Provide database credentials** (database must be empty).\n4. **Select language** (Italian, English, French, or German).\n5. **Configure** organization name, logo, and email notifications.\n6. **Create admin account** and start cataloging.\n\n**Email configuration**: Supports both PHP `mail()` and SMTP. Required for notifications to work (loan confirmations, due-date reminders, registration approvals, reservation alerts). Can be configured during installation or later from the admin panel.\n\n**Post-install** (optional but recommended):\n- Remove/lock the `installer/` directory (button provided on final step)\n- Configure SMTP, registration policy, and CMS blocks from **Admin → Settings**\n- Schedule cron jobs for automated tasks:\n  ```bash\n  # Notifications every hour (8 AM - 8 PM)\n  0 8-20 * * * /usr/bin/php /path/to/cron/automatic-notifications.php \u003e\u003e /var/log/biblioteca-cron.log 2\u003e\u00261\n\n  # Full maintenance at 6 AM (handles reservation/pickup expirations)\n  0 6 * * * /usr/bin/php /path/to/cron/full-maintenance.php \u003e\u003e /var/log/biblioteca-maintenance.log 2\u003e\u00261\n  ```\n\nAll frontend assets are precompiled. Works on shared hosting. No Composer or build tools required on production. All configuration values can be changed later from the admin panel.\n\n---\n\n## Story Behind the Name\n\n**Pinakes** comes from the ancient Greek word *πίνακες* (\"tables\" or \"catalogues\"). Callimachus of Cyrene compiled the *Pinakes* around 245 BC for the Library of Alexandria: 120 scrolls that indexed more than 120,000 works with authorship, subject and location. This project borrows that same mission—organising and sharing knowledge with modern tools.\n\n**Full documentation**: [fabiodalez-dev.github.io/Pinakes](https://fabiodalez-dev.github.io/Pinakes/)\n\n---\n\n## What It Does\n\nPinakes provides cataloging, circulation, a self-service public frontend, and REST APIs out of the box. It ships with precompiled frontend assets and a guided installer so you can deploy quickly on standard LAMP hosting.\n\n---\n\n## Screenshots\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./docs/dashboard.png\" alt=\"Admin Dashboard\" width=\"800\"\u003e\u003cbr\u003e\n  \u003cem\u003eAdmin Dashboard — Loans overview, calendar, and quick stats\u003c/em\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./docs/books.png\" alt=\"Book Catalog Management\" width=\"800\"\u003e\u003cbr\u003e\n  \u003cem\u003eBook Management — ISBN auto-fill, multi-copy support, cover retrieval\u003c/em\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./docs/catalog.png\" alt=\"Public Catalog\" width=\"800\"\u003e\u003cbr\u003e\n  \u003cem\u003ePublic Catalog (OPAC) — Search, filters, and patron self-service\u003c/em\u003e\n\u003c/p\u003e\n\n---\n\n## Core Features\n\n### Automatic Metadata Import\n- **ISBN/EAN scraping** from Google Books, Open Library, and pluggable sources\n- **Automatic cover retrieval** when available\n- **Every field editable manually** — automation never locks you in\n\n### Cataloging\n- **Multi-copy support** with independent barcodes and statuses for each physical copy\n- **Unified records** for physical books, eBooks, and audiobooks\n- **Dewey Decimal Classification** with 1,200+ preset categories (IT/EN), hierarchical browsing, manual entry for custom codes, and auto-population from SBN scraping\n- **CSV bulk import** with field mapping, validation, automatic ISBN enrichment, and Dewey classification from scraping\n- **LibraryThing TSV import** with flexible column mapping for 29 custom fields and automatic metadata enrichment\n- **Import history** — Admin panel with per-import statistics, error reports downloadable as CSV, and log retention management\n- **Automatic duplicate detection** by ID, ISBN13, EAN, or title+author (updates existing records without modifying physical copies)\n- **Author and publisher management** with dedicated profiles and bibliography views\n- **Genre/category system** with custom taxonomies and multi-category assignment\n- **Series and collections** tracking with sequential numbering\n- **Barcode generation** for physical inventory (Code 128, EAN-13, custom formats)\n- **Cover image management** with automatic download, manual upload, and URL import\n- **Rich metadata fields** including edition, publication date, language, format, dimensions, weight, page count\n- **Keywords and tags** for enhanced searchability and subject indexing\n- **Custom notes and annotations** for internal cataloging remarks\n\n### Circulation\n- **Full loan workflow**: request, approval, checkout, renewal, return\n- **Automatic due-date calculation** with configurable loan periods\n- **Configurable renewal rules** (manual or automatic approval)\n- **FIFO reservation queues** with availability alerts when items become free\n- **Detailed per-user and per-item history** for audit trails\n\n### Catalogue Mode\n- **Browse-only option** for libraries that don't need circulation features\n- **Configurable during installation** or via Admin → Settings → Advanced\n- **Hides all loan-related UI**: request buttons, reservation forms, wishlist\n- **Admin sidebar simplified** without loan management menus\n- **Perfect for**: digital archives, reference-only collections, museum libraries\n\n### Pickup Confirmation System\n- **New `ready_for_pickup` state** — Approved loans enter \"Ready for Pickup\" before becoming active\n- **Two-step workflow** — Admin approves → Patron picks up → Admin confirms pickup\n- **Configurable pickup deadline** — Days allowed for pickup (Settings → Loans, default: 3 days)\n- **Cancel pickup** — Admin can cancel uncollected loans, freeing copy and advancing reservation queue\n- **Automatic queue advancement** — Next patron notified immediately when pickup is cancelled\n- **Works without cron** — Real-time queue processing, no maintenance service dependency\n- **Visual indicators** — Orange badge for \"Ready for Pickup\" in all loan views\n- **Calendar integration** — `ready_for_pickup` periods shown in orange, block availability for other reservations\n- **Origin tracking** — System tracks whether loans originated from reservations or manual creation\n\n### Calendar \u0026 ICS Integration\n- **Interactive dashboard calendar** (FullCalendar) showing all loans and reservations\n- **Color-coded events**: active loans (green), scheduled (blue), overdue (red), pending requests (amber), reservations (purple)\n- **Start/end markers** for easy visualization of loan periods\n- **Click to view details**: user, book title, dates, and status in modal popup\n- **ICS calendar export** for syncing with external calendar apps (Google Calendar, Apple Calendar, Outlook)\n- **Automatic ICS generation** via maintenance service or cron job\n- **Subscribable calendar URL** that stays updated with latest loans and reservations\n\n### Email Notifications\nAutomatic emails for:\n- New user registration\n- Registration approval\n- Loan confirmation\n- Approaching due dates (configurable days before)\n- Overdue reminders\n- Item-available notifications for reservations\n\n**WYSIWYG email template editor** with dynamic tags for record, user, and loan data.\n\n### Public Catalog (OPAC)\n- **Responsive, multilingual frontend** (Italian, English, French, German)\n- **AJAX search** with instant results and relevance ranking\n- **AJAX filters**: genre, publisher, availability, publication year, format\n- **Patrons can leave reviews and ratings** (configurable)\n- **Built-in SEO tooling**: sitemap, clean URLs, Schema.org JSON-LD (Book, BreadcrumbList, Event), hreflang tags, RSS 2.0 feed, `/llms.txt` for AI crawlers\n- **Cookie-consent banner** and privacy tools (GDPR-compliant)\n\n### Dewey Decimal Classification\n- **1,200+ preset categories** in Italian and English loaded from JSON files\n- **Hierarchical browsing** — Navigate from main classes (000-999) to subdivisions (e.g., 599.9 Mammals)\n- **Manual entry** — Accept any valid Dewey code, not limited to preset list\n- **Format validation** — Real-time validation of code format (XXX.XXXX)\n- **Automatic population from SBN** — Dewey codes extracted during ISBN scraping are auto-added to the database\n- **Multi-language** — Separate JSON files for IT/EN with full translations\n- **Dewey Editor plugin** — Visual tree editor for managing classifications with import/export\n- **No database table** — Data loaded from `data/dewey/` JSON files at runtime\n\n### Auto-Updater\n- **Built-in update system** — Check, download, and install updates from Admin → Updates\n- **Manual ZIP upload** — Upload `.zip` release packages for air-gapped or rate-limited environments\n- **Automatic database backup** — Full MySQL dump before every update\n- **Safe file updates** — Protected paths (.env, uploads, storage) are never overwritten\n- **Database migrations** — Automatic execution of SQL migrations for version jumps\n- **Atomic rollback** — Automatic restore on error with pre-update backup\n- **Orphan cleanup** — Files removed in new versions are deleted from installation\n- **OpCache reset** — Automatic cache invalidation after file updates\n- **Security** — CSRF validation, admin-only access, path traversal protection, Zip Slip prevention\n- **GitHub API token** — Optional personal access token (Admin → Updates) to raise GitHub API rate limits from 60 to 5,000 req/hr\n\n### Physical Inventory\n- **Hierarchical location model**: shelf, aisle, position\n- **Automatic position assignment** for new copies\n- **Barcode generation** in standard formats\n- **Printable PDF labels** in multiple sizes (customizable templates)\n\n### Digital Content\n- **eBook distribution** (PDF, ePub) with download tracking\n- **Audiobook streaming** (MP3, M4A, OGG) with integrated player\n- **Drag-and-drop upload** or external URL linking\n\n### Archival Records — ISAD(G) + ISAAR(CPF)\n\nShipped as the bundled **Archives** plugin (opt-in; activate from Admin → Plugins). Lets the same Pinakes install manage both a book catalogue *and* a hierarchical archive — fonds, series, files, items — according to the international archival standards used by public archives, historical societies, photographic collections, and academic repositories.\n\n**Hierarchical archival description (ISAD(G) 2nd ed.)**\n- Four-level hierarchy: `fonds` → `series` → `file` → `item`. Each row is a standalone ISAD(G) record with `parent_id` chaining up to an arbitrary depth (real archives are usually 2-4 deep).\n- Full identity area (3.1): reference code, institution code, formal + constructed title, date range (start/end + predominant dates + significant gaps), extent, language codes.\n- Context \u0026 content (3.2-3.3): archival history, acquisition source, scope \u0026 content, appraisal/destruction schedule, accruals policy, system of arrangement.\n- Access \u0026 use (3.4): access conditions, reproduction rules, language/script notes, physical characteristics, finding aids.\n- Allied materials (3.5): originals/copies location, related units.\n- Soft-delete aligned with the library-side `libri` convention (deleted rows vanish from views, still queryable for restore).\n- Descendant-cycle guard: an edit that would make a unit its own descendant is rejected with a validation error (walks ancestors up to 100 hops).\n\n**Authority records (ISAAR(CPF))**\n- Dedicated table, separate from `autori`, because ISAAR covers persons **and** corporate bodies **and** families — a richer element set than bibliographic authors.\n- Identity (5.1): type, authorised form, parallel forms, other forms, identifiers (VIAF / ISNI / ORCID).\n- Context (5.2): dates of existence, history, places, legal status, functions/occupations, mandates, internal structure/genealogy, general context, gender.\n- M:N linking to archival units with MARC-aligned roles: `creator` / `subject` / `recipient` / `custodian` / `associated`.\n- Cross-reconciliation with the library-side `autori` table via `autori_authority_link` — unifies books and archives under a single person/entity in the public search.\n\n**Photographic \u0026 audio-visual materials (ABA billedmarc)**\n- `specific_material` ENUM with 15 ABA codes: text (bf), photograph (hf), poster (hp), postcard (hm), drawing (hd), map (hk), picture (hb), 3D object/realia (ho), audio recording (lm), motion-picture film (lf), video (vm), microform (bm), electronic/born-digital (le), mixed materials (zz), other.\n- Dedicated columns for colour mode (bw / colour / mixed), dimensions, photographer, publisher, collection name, local classification — matching the MARC 300/337/338 content/media/carrier vocabulary.\n\n**Per-document digital assets** (v0.7.6+)\n- Each archival unit can hold a cover image plus multiple downloadable digitised files (PDF / ePub / audio / video). Files are stored under `public/storage/archives/{unit_id}/` with original filename, MIME type, and display order. Drag-and-drop upload from the admin form; per-file delete with admin confirmation.\n\n**Multi-format export — MARCXML, EAD3, METS, UNIMARC, Dublin Core**\n- **MARCXML** (Library of Congress MARC21 Slim): `GET /admin/archives/{id}/export.xml` and `GET /admin/archives/export.xml?ids=…` emit ABA-crosswalk MARCXML via XMLWriter. Authorities exported as 100/110/600/610/700/710 tags depending on `(type, role)`.\n- **EAD3** (archivists.org Encoded Archival Description): `GET /admin/archives/export.ead3` emits a full EAD3 finding aid. Mirrors what AtoM, ArchivesSpace, and the national portals consume.\n- **METS** (Library of Congress Metadata Encoding \u0026 Transmission): `GET /archives/{id}/mets.xml` packages descriptive metadata + IIIF manifest link + digitised assets into a single METS document for OAI-PMH MAG harvesting (Internet Culturale / ICCU).\n- **UNIMARC** (IFLA): exposed via SRU/OAI-PMH for federation with BNF and Italian SBN partners.\n- **Dublin Core** (oai_dc): `GET /archives/{id}/dc.xml` and via OAI-PMH below.\n- **Import**: `POST /admin/archives/import` parses MARCXML (SimpleXML) with optional XSD validation against the Library of Congress MARC21 Slim v1.1 schema. UPSERT on `(institution_code, reference_code)` — re-importing the same file is idempotent. Dry-run preview available.\n\n**SRU 1.2 read-only endpoint**\n- `GET /api/archives/sru` — supports `explain`, `searchRetrieve` (CQL subset: `title`, `reference`, `level`, `anywhere`, joined with `AND`), and `scan` stub. External catalogues (Reindex, Koha, ARKIS, BNF) can federate-search the archive using MARCXML records.\n\n**IIIF Presentation 3.0** ([#123](https://github.com/fabiodalez-dev/Pinakes/issues/123), v0.7.6+)\n- `GET /archives/{id}/manifest.json` returns a standards-compliant IIIF Presentation 3.0 manifest for every archival unit, exposing attached digitised documents as `Canvas` items with painting `Annotation`s. Works out of the box with **Universal Viewer**, **Mirador**, and any other IIIF-compatible viewer.\n- `GET /archives/collection.json` and `GET /archives/{id}/collection.json` emit IIIF Collection documents for the root fonds list and per-unit sub-collections.\n- The manifest's `seeAlso` block points to every alternative serialisation (Dublin Core, EAD3, METS, RiC-O, OAI-PMH record, ARK identifier) so an IIIF-aware client can discover the full graph of metadata representations with no second discovery round-trip.\n\n**RiC-O JSON-LD** (Records in Contexts Ontology, ICA 2023 — [#122](https://github.com/fabiodalez-dev/Pinakes/issues/122), v0.7.7+)\n- `GET /archives/{id}/ric.json` and `GET /archives/agents/{id}/ric.json` emit RiC-O JSON-LD for archival units and authority records — the linked-data successor to ISAD(G)/ISAAR. Each role on `archival_unit_authority` maps to a typed predicate (`ric:isCreatorOf`, `ric:isOrWasCustodianOf`, `ric:isSubjectOf`, `ric:isAddresseeOf`, `ric:isAssociatedWith`).\n- `GET /archives/collection.ric.json` emits a synthetic `ric:RecordSet` aggregating all top-level fonds, suitable for harvesting by Europeana, ArchivesPortalEurope, and the ICA aggregator.\n- `owl:sameAs` URIs are gathered transparently from the `viaf-authority` plugin's authority links and filtered through a strict scheme allow-list (`http(s)`, `urn`, `ark`, `info`, `doi`) before emission.\n\n**AtoM ISAD(G) area labels** ([#121](https://github.com/fabiodalez-dev/Pinakes/issues/121), v0.7.6+)\n- The admin UI and public display now use the canonical ISAD(G) area names (`Identity area`, `Context area`, `Content and structure area`, `Conditions of access and use area`, `Allied materials area`, `Notes area`), so records are immediately recognisable to users coming from AtoM or other archival catalogue software.\n\n**ARK persistent identifiers + rightsstatements.org** (v0.7.x)\n- Each archival unit can carry an ARK identifier (Archival Resource Key) — emitted as `https://n2t.net/{ark}` in the IIIF `seeAlso` and the RiC-O `rdfs:seeAlso` blocks.\n- Rights are expressed via a `rightsstatements.org` URI (e.g., `https://rightsstatements.org/vocab/InC/1.0/`) — mapped to `ric:Rule` + `owl:sameAs` in the RiC-O output.\n\n**Unified cross-entity search**\n- `/admin/archives/search` hits three sources in a single query: `archival_units` (FULLTEXT on title + scope + archival_history), `authority_records` (FULLTEXT on authorised_form + history + functions), and `autori` rows reconciled to an authority.\n\n**Search bars (admin + public)**\n- **Admin** (`/admin/archives?q=…\u0026level=…`): two-pass query — LIKE on `reference_code` first (short archival codes like `IT-MI-001` are below FULLTEXT's `ft_min_word_len`), then FULLTEXT on title/scope/history. Level filter narrows to one archival tier. Search mode renders results as a flat list (no hierarchy indent). Result counter and input persistence.\n- **Public** (`/archivio?q=…\u0026level=…\u0026date_from=…\u0026date_to=…`): same text + level filters plus date-range overlap (`date_from` / `date_to`). In search mode all hierarchy levels are returned (not just root fonds), so a user can search directly for a series or fascicolo by reference code. Theme-aware CSS.\n\n**OAI-PMH 2.0 data provider**\n- `GET /oai` (exposed by the OAI-PMH server plugin) advertises archival units alongside book records via `set=archives`: `Identify`, `ListMetadataFormats` (oai_dc + marc21 + ead3 + ric-o for archival_unit), `ListSets` (per ISAD level), `ListRecords`/`GetRecord` with resumption tokens, selective harvesting by set + date range.\n\n**Plugin integration**\n- Self-contained at `storage/plugins/archives/`. Wires up through two `plugin_hooks` rows (`app.routes.register`, `admin.menu.render`) on activation; deactivation removes the route + sidebar entry without touching DB data.\n- Full i18n (IT/EN/DE) with ICA-ISAD(G) terminology (IT) / ICA (EN) / ICA-Deutsch (DE: Bestand / Signatur / Einzelstück).\n- Migration `migrate_0.5.9.sql` is fully idempotent (INFORMATION_SCHEMA guards + conditional ALTERs) — safe to re-run on partial installs, cleanly extends the ENUM on upgrades.\n\n### Plugin System\nExtend without modifying core files. Plugins can implement:\n- New metadata scrapers (custom APIs, proprietary databases)\n- Additional business logic (custom loan rules, notifications)\n- Digital-content modules (eBooks, audiobooks, streaming)\n- Import/export routines (MARC21, UNIMARC, custom formats)\n\nPlugins support encrypted secrets and isolated configuration. Install via ZIP upload in admin panel.\n\n**Pre-installed plugins** (16 included — every one opt-in via Admin → Plugins; the only one auto-active is Open Library):\n\n*Metadata scraping \u0026 enrichment*\n- **Open Library** — Metadata scraping from Open Library + Google Books API; the default scraper\n- **API Book Scraper** — External ISBN enrichment via configurable REST endpoints\n- **Discogs / MusicBrainz / Deezer** — Music scrapers for CDs, LPs, vinyls, cassettes (barcode + title lookup, Cover Art Archive HD jackets, tracklists, label info)\n- **GoodLib** — One-click cross-search badges on the book detail page (Anna's Archive, Z-Library, Project Gutenberg)\n- **VIAF Authority Control** — Links authors to VIAF/ISNI authority records with confidence scoring + W3C reconciliation API\n\n*Interoperability protocols*\n- **Z39 Server** — SRU 1.2 API + Z39.50/SRU client for Italian SBN, French **BNF** (UNIMARC), and any standard library catalogue with Dewey extraction (v0.7.6+)\n- **OAI-PMH Server** — OAI-PMH 2.0 data provider for books + archives, harvestable by Internet Culturale (ICCU), Europeana, DPLA. Formats: `oai_dc`, `marc21`, `mods`, `mag` (2.0.1), `unimarc`, `ead3`, `ric-o` (RDF/XML, archival units)\n- **NCIP 2.0 Server** — NISO Circulation Interchange Protocol for self-service kiosks, partner ILS, and library networks\n- **BIBFRAME 2.0 Linked Data** — Exposes the book catalogue as BIBFRAME 2.0 JSON-LD / Turtle with content negotiation (Library of Congress transition path from MARC)\n- **OpenURL Resolver** — Z39.88-2004 resolver + COinS metadata embedded in book pages; works with Zotero, Mendeley, EndNote\n- **ResourceSync** — ANSI/NISO Z39.99-2014 dataset synchronisation for harvester partners\n\n*Cataloging \u0026 specialised collections*\n- **Dewey Editor** — Visual tree editor for Dewey classification data with JSON import/export and auto-population from SBN/SRU scraping\n- **Digital Library** — eBook (PDF, ePub) and audiobook (MP3, M4A, OGG) management with HTML5 streaming player\n- **Archives** — ISAD(G) hierarchical archival records + ISAAR(CPF) authority records. MARCXML / EAD3 / METS / UNIMARC / Dublin Core export, SRU 1.2 endpoint, OAI-PMH 2.0 data provider, **IIIF Presentation 3.0** manifests (v0.7.6), **RiC-O JSON-LD** export (v0.7.7), AtoM ISAD(G) area labels, ARK persistent identifiers, full-text + reference-code search bar (admin + public with date-range filter), photographic-material support (ABA billedmarc 15 codes), per-document cover + downloadable file uploads, and unified cross-entity search bridging books + archives\n\n### CMS and Customization\n- **Homepage editor** with drag-and-drop blocks (hero banner, featured shelves, events, testimonials)\n- **Custom pages** (About, Regulations, Events) with WYSIWYG editing\n- **10 color themes** with instant switching (Sky Blue, Forest Green, Royal Purple, Sunset Orange, Cherry Red, Ocean Teal, Lavender Dreams, Midnight, Coral Sunset, Golden Hour)\n- **Custom theme editor** with live preview for primary, secondary, and CTA colors\n- **Logo customization** and branding\n- **Centralized media manager** for images and documents\n- **Event management** with dates, descriptions, and homepage display\n\n### APIs\n- **REST API** for search, availability, cataloging, and statistics\n- **SRU 1.2 protocol** at `/api/sru` — standard library interoperability (MARCXML, Dublin Core, MODS export)\n- **Z39.50 client** for copy cataloging from external catalogs (Library of Congress, OCLC, national libraries)\n- **CSV and Excel export** for reports and backups\n- **PDF generation** for labels, receipts, and reports\n\n### User Management\n- **Manual approval** of new registrations (optional)\n- **Automatic card number assignment** with customizable prefixes\n- **Complete per-user history** of loans and reservations\n- **Self-service patron portal** with loan renewal, reservation management, and wishlists\n\n---\n\n## Why It Might Be Useful\n\n- **Fast ISBN-driven cataloging** cuts manual entry to seconds per book\n- **Usable public catalog** without needing a web developer or custom theme work\n- **Extensible via plugins** if you want custom scrapers, integrations, or business logic\n- **Self-hosted and GPL-3 licensed** — full control, no vendor lock-in, no recurring fees\n- **Works on shared hosting** — no root access, Docker, or build tools required on production\n\n---\n\n## Plugins (Pre-installed)\n\nAll plugins are located in `storage/plugins/` and can be managed from **Admin → Plugins**.\n\n### 1. Open Library (`open-library-v1.0.1.zip`)\n- **Metadata scraping** from Open Library API\n- **Fallback to Google Books** when Open Library lacks data\n- **Automatic cover download** with validation\n- **Subject mapping** and language normalization\n- **Configurable priority** and caching options\n\n### 2. Z39 Server (`z39-server-v1.2.3.zip`)\n\nImplements the **SRU (Search/Retrieve via URL)** protocol, the HTTP-based successor to Z39.50, enabling catalog interoperability with library systems worldwide.\n\n**Server Mode** (expose your catalog):\n- **SRU 1.2 API** at `/api/sru` with explain, searchRetrieve, scan operations\n- **Export formats**: MARCXML, Dublin Core, MODS, OAI_DC\n- **CQL query parser** supporting indexes: `dc.title`, `dc.creator`, `dc.subject`, `bath.isbn`, `dc.publisher`, `dc.date`\n- **Rate limiting** (100 req/hour per IP) and comprehensive access logging\n- **Optional API key authentication** via `X-API-Key` header\n- **Trusted proxy support** for deployments behind load balancers (CIDR notation)\n\n**Client Mode** (import from external catalogs):\n- **Copy cataloging** from Z39.50/SRU servers (Library of Congress, OCLC, K10plus, SUDOC, national libraries)\n- **SBN Italia client** — Automatic metadata retrieval from Italian national library catalog\n- **BNF (Bibliothèque nationale de France) client** (v0.7.6+) — UNIMARC scraping from the BNF SRU endpoint with field mapping to Pinakes metadata (title, authors, publisher, ISBN, Dewey, subjects). Enable Z39 Server and add `sru.bnf.fr` as a source to start importing French bibliographic records.\n- **UNIMARC parser** — Handles UNIMARC field codes (200, 210, 215, 700/701/702, 676 for Dewey) in addition to MARC21, so French + Italian + Swiss + Belgian + Quebecois catalogues are all consumable\n- **Dewey classification extraction**:\n  - SBN: Parses Dewey codes from `classificazioneDewey` field (format: `335.4092 (19.) SISTEMI MARXIANI`)\n  - SRU/MARCXML: Extracts from MARC field 082 (Dewey Decimal Classification Number)\n  - UNIMARC: Extracts from field 676 (BNF Dewey representation)\n  - Dublin Core: Parses from `dc:subject` (DDC scheme) and `dc:coverage` fields\n- **Federated search** across multiple configured servers\n- **Automatic retry** with exponential backoff (100ms, 200ms, 400ms)\n- **TLS certificate validation** for secure connections\n- **MARCXML, UNIMARC, and Dublin Core parsing** with author extraction (MARC 100/700, UNIMARC 700/701/702 fields)\n- **CQL injection hardening** (v0.7.6+) — search terms containing `\"` or `\\` are properly escaped per the CQL specification before being embedded into queries sent to external SRU endpoints\n\n**Example queries**:\n```bash\n# Server info\ncurl \"http://yoursite.com/api/sru?operation=explain\"\n\n# Search by author\ncurl \"http://yoursite.com/api/sru?operation=searchRetrieve\u0026query=dc.creator=marx\u0026maximumRecords=10\"\n\n# Search by ISBN (Dublin Core format)\ncurl \"http://yoursite.com/api/sru?operation=searchRetrieve\u0026query=bath.isbn=9788842058946\u0026recordSchema=dc\"\n```\n\n**Use cases**: Union catalogs, interlibrary loan systems, OPAC federation, copy cataloging workflows, automatic Dewey classification.\n\n### 3. API Book Scraper (`api-book-scraper-v1.1.1.zip`)\n- **External API integration** for ISBN enrichment\n- **Custom endpoint configuration** (URL, headers, auth)\n- **Response mapping** to Pinakes schema\n- **Retry logic** with exponential backoff\n- **Error logging** and debugging tools\n\n### 4. Digital Library (`digital-library-v1.3.0.zip`)\n- **eBook support** (PDF, ePub) with download tracking\n- **Audiobook streaming** (MP3, M4A, OGG) with HTML5 player\n- **Per-item digital asset management** (unlimited files per book)\n- **Access control** (public, logged-in users only, specific roles)\n- **Usage statistics** and download history\n\n### 5. Dewey Editor (`dewey-editor-v1.0.1.zip`)\n\nComplete Dewey Decimal Classification management system with multilingual support, automatic population, and data exchange capabilities.\n\n**Core Features**:\n- **Tree-based visual editor** — Navigate and edit the complete Dewey hierarchy (1,200+ preset entries)\n- **Multi-language support** — Separate JSON files for Italian (`dewey_completo_it.json`) and English (`dewey_completo_en.json`) with full translations\n- **Inline editing** — Add, modify, or delete categories with instant validation\n- **Validation engine** — Checks code format (XXX.XXXX), hierarchy consistency, and duplicate detection\n\n**Data Exchange**:\n- **JSON import/export** — Backup and restore classification data for manual editing or exchange with other Pinakes installations\n- **Cross-installation sharing** — Export your customized Dewey database and import it into another Pinakes instance\n- **Merge capability** — Import external classifications while preserving existing entries\n\n**Automatic Dewey Scraping**:\n- **SBN integration** — When scraping book metadata from SBN (Italian National Library), Dewey codes are automatically extracted from the `classificazioneDewey` field\n- **SRU/Z39.50 servers** — Dewey codes extracted from MARC field 082 when querying external catalogs (K10plus, SUDOC, Library of Congress, etc.)\n- **Auto-population** — New Dewey codes discovered during scraping are automatically added to your JSON database (language-aware: only updates when source language matches app locale)\n- **CSV import enrichment** — Books imported via CSV are automatically enriched with Dewey classification through ISBN scraping\n\n**Dewey Code Format**:\n- Main classes: `000`-`999` (3 digits)\n- Subdivisions: `000.1` to `999.9999` (up to 4 decimal places)\n- Examples: `599.9` (Mammiferi/Mammals), `004.6782` (Cloud computing), `641.5945` (Cucina italiana/Italian cuisine)\n\n**Book Form Integration**:\n- **Chip-based selection** — Selected Dewey code displays as removable chip with code + name\n- **Manual entry** — Accept any valid Dewey code (not limited to predefined list)\n- **Hierarchical navigation** — Optional collapsible \"Browse categories\" for discovering codes\n- **Breadcrumb display** — Shows full classification path (e.g., \"500 → 590 → 599 → 599.9\")\n- **Frontend validation** — Real-time format validation before submission\n\n### 6. Archives (`archives`)\n\nISAD(G) / ISAAR(CPF) hierarchical archival records — see [Archival Records](#archival-records","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffabiodalez-dev%2Fpinakes","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffabiodalez-dev%2Fpinakes","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffabiodalez-dev%2Fpinakes/lists"}