Releases: fabiodalez-dev/Pinakes
Release list
Pinakes v0.7.71-rc.2
Fixes the LiteSpeed edge cache so the admin toggle actually caches, and adds an on-demand purge control. No schema change.
Performance
- The edge-cache privacy block shipped in 0.7.70/0.7.71 without
CacheLookup on, so LiteSpeed ignored the application's cache headers and never served a page from edge cache — the Settings → Advanced toggle enabled the response headers but stored nothing. The generated, fresh-install and example.htaccessnow enable LSCache request lookup, and an upgraded install heals its existing block in place (no manual re-save) so an admin who had LiteSpeed enabled starts caching immediately. LiteSpeedCache::enabled()now additionally requires the lookup directive to be present, so the admin diagnostics and the front-end pending-availability fallbacks only treat caching as live when the server will actually honour it. The Apache-based official Docker image continues to disable LiteSpeed entirely.
Administration
- Settings → Advanced gains a "Clear edge cache" button that purges every Pinakes-tagged LiteSpeed entry on demand, for when an admin wants an immediate refresh instead of waiting for content-change invalidation. Translated in every bundled locale.
Internal
- New behavioral coverage verifies that a fresh install enables lookup, that a legacy privacy block missing
CacheLookup onself-heals in place exactly once and idempotently, and that the purge control is wired end to end.
The ZIP was built twice with an identical SHA-256, audited as the
exact distributable, accompanied by an SPDX SBOM, and signed with
a GitHub artifact provenance attestation.
Pinakes v0.7.71-rc.1
First release candidate for catalog denormalization. Existing installs receive an idempotent migration; Redis remains deliberately deferred.
Performance
- Bounded catalog counts and facet payloads are materialized in MySQL with the same generation token and short safety TTL used by
QueryCache. APCu remains the fastest same-host layer, while CLI/file-cache SAPIs that share the installation generation reuse the aggregate instead of repeating six catalog scans. Cross-server coherence remains deferred to the Redis phase. - Public catalog rows read a write-maintained principal-author projection instead of executing three correlated
libri_autorisubqueries per book. Principal/co-author priority, explicit credit order, pseudonym display and author sorting remain deterministic.
Compatibility and consistency
- Book, author, import and enrichment write paths rebuild the author projection through the existing
SearchIndexBuildermaintenance funnel. During a rolling upgrade, catalog queries retain the legacy subquery fallback until all projection columns exist. - Materialized aggregates fail open to live SQL if the table, generation counter or MySQL named lock is unavailable. Content and availability invalidation still use
ContentCache; no second availability source is introduced. - Projection degradation tracking now coordinates readers and writers through a stable sibling lock and fails closed on partial or corrupt state. Profile review-cache invalidation compares against the locked database before-value instead of a potentially stale session.
- The Apache-based official Docker image permanently disables LiteSpeed/LSCache at runtime and in Settings, even when an imported database or forged request asks to enable it; file/APCu query caching remains available normally.
The ZIP was built twice with an identical SHA-256, audited as the
exact distributable, accompanied by an SPDX SBOM, and signed with
a GitHub artifact provenance attestation.
Pinakes v0.7.69-rc.6
Sixth release candidate for the issue #387 caching overhaul. No schema change and no new required configuration.
Fixed
- The circular prerelease-check exception now recognizes only an actively pending or failed Verified Release run. A cancelled, skipped or neutral self-check can no longer satisfy the
BLOCKEDsafeguard; regression tests cover both terminal buckets.
The ZIP was built twice with an identical SHA-256, audited as the
exact distributable, accompanied by an SPDX SBOM, and signed with
a GitHub artifact provenance attestation.
Pinakes v0.7.69-rc.1
Release candidate for a performance/caching overhaul (issue #387). No schema change, no new required configuration — every install upgrades with identical behaviour on a cold cache. Bundles three reviewed pieces:
Performance
- Single-backend query cache with O(1) invalidation.
QueryCachenow uses one backend per request (APCu when available, else file) instead of writing to both on every set, and invalidates the content namespaces (catalog, home, genre-tree, book-detail, reviews) through monotonic generation counters — a bump makes every prior entry unreachable without scanning or deleting. The counters are file-backed and written atomically (temp +rename()), so a failed write can never wipe one and re-expose invalidated data; a scheduled non-blocking GC reclaims orphaned files. Per-request hit/miss instrumentation (QueryCache::stats()) is available for a later diagnostics surface. - Hot public datasets are cached, availability stays live. The book-detail static DTO (metadata/authors/publishers/series/related), the reviews block and the bounded catalog listing pages are cached per locale, while
copie_disponibili/copie_totali/statoare stripped before storage and re-read live on every request — a stale availability number can never be served. Every write-path that changes a cached column (book edit, cover download, series mutation, reviewer name, author enrichment) invalidates the right namespace. - Sessionless anonymous request path + lazy CSRF. Anonymous read-only requests with no auth cookie no longer open a PHP session or embed a per-visitor CSRF token in the HTML (the token is minted on demand via
GET /csrf-token), clearing the two blockers to a future shared edge cache. CSRF validation is unchanged — every state-changing request still opens a session and is validated exactly as before; private-mode gating and the login/remember-me flows are untouched. Only the genuinely public/uploadssubtrees (covers, author photos, branding) are treated as sessionless; the private ones stay behind the session.
Internal
- OPcache guidance in
php.ini.recommendedcorrected (JIT explicitly disabled,fast_shutdownremoved, sizing driven byopcache_get_status()). Extensive new behavioural coverage for the cache backend, generation invalidation, atomicity, the availability split and the sessionless/CSRF predicate.
The ZIP was built twice with an identical SHA-256, audited as the
exact distributable, accompanied by an SPDX SBOM, and signed with
a GitHub artifact provenance attestation.
Pinakes v0.7.68
Circulation fixes. A request for a book that is currently out no longer fails on approval — it becomes a real waitlist reservation — and copy allocation is made consistent everywhere a copy is chosen. Also restores the "waiting for pickup" cancel action (#381), the terminal pickup email for archived books, and author photos supplied as a URL (#382).
Fixed
- A request for an already-loaned single-copy book now routes to the waitlist instead of failing on approval (#384). The request used to be recorded as a pending loan, and approving it returned HTTP 500 because no copy could be allocated for the overlapping window. The reservation-vs-loan decision is now unified in one gate: when no copy is assignable through the requested window but physical copies exist, a real
prenotazioniwaitlist reservation is created; when a copy is free the loan proceeds (auto-approving where enabled). Legacy copy-less titles keep the pending-approval fallback. - Copy allocation is consistent across every site that picks a copy (#384). The request gate, the approval step and the FIFO waitlist promotion now share one rule: a copy carrying a preceding commitment — one whose availability would depend on an earlier borrower returning on time — is never bound, and among otherwise-free copies the one with no later scheduled loan is preferred. A future scheduled loan therefore never comes to depend on a new borrower returning early, and an idle sibling is used instead of the copy a later loan already needs.
- A "waiting for pickup" loan can be cancelled again (#381). The cancel action resolves to
annullato— orscadutowhen the pickup deadline has already passed — restores the copy to availability, and never interferes with other loans or later reservations on the same or another copy. - The terminal pickup email reaches the borrower even for a soft-deleted book. The cancel/expire notification query filtered out soft-deleted books, so a borrower whose book had since been archived silently lost the expired/cancelled email even though the loan was closed; the notice now fires regardless.
- Author photos supplied as a URL now display (#382). A photo entered as a remote URL is downloaded into
uploads/autorion save instead of being stored as a bare link the page could not render.
Internal
- The reservation-vs-loan decision now lives in a single shared
LoanRequestGatethat every public request entry point routes through (the book-detail modal,POST /user/loan, and the NCIPRequestItemservice), so the #384 contract can never diverge between paths. NCIPRequestItemcreates a real FIFO reservation when no copy is assignable instead of a bare pending request;ncip_transactionsgains aprenotazione_idaudit link (added to existing installs by the plugin schema self-heal), andCancelRequestItemcancels the linked reservation, reorders the queue and runs any promotions. Bundled ncip-server plugin bumped to 1.0.4. - The companion Mobile API now preserves that same outcome at the JSON boundary: when the canonical loan path routes an apparently-immediate request to the FIFO waitlist it returns HTTP 201 with
data.type=reservation, never a false 500 after committing the reservation. Mobile API 1.4.4 also exposesDELETE /loans/{id}and acancellableloan hint so native clients can safely cancel pending, scheduled andda_ritirareloans (#381) without relying on the overlapping numeric id spaces of loans and reservations. - New behavioural coverage for the reservation-routing gate, the three copy-selection sites (I1 preceding-dependency and I3 future-commitment ordering), the soft-deleted-book pickup notification and the NCIP request/cancel lifecycle, each driving the real controllers against a seeded database and failing on the pre-fix code by design.
The ZIP was built twice with an identical SHA-256, audited as the
exact distributable, accompanied by an SPDX SBOM, and signed with
a GitHub artifact provenance attestation.
Pinakes v0.7.67
Fixes a silent upgrade gap discovered on a live install: a foreign key or column that a bundled plugin adds in a release was not applied when an already-active plugin was upgraded through the admin UI, even though the plugin version was bumped.
Fixed
- Plugin schema self-heal now covers foreign keys and columns, not just tables. An admin-UI upgrade loads a plugin's old class into memory before the new files are swapped in, so PHP keeps running the old
onActivate/ensureSchemaduring the upgrade even as the version is stamped forward — any foreign key or column added in that release never runs at upgrade time. The boot-time self-heal is the only path that applies it, and it previously probed missing tables only. It now also probes declared columns (expectedColumns()) and foreign keys (expectedForeignKeys()) and re-runsonActivatewhen any declared element is missing. Concrete effect:ncip_transactions' two foreign keys andlibri.file_url/libri.audio_urlare now restored automatically on the next page load after upgrading. No action is required; already-upgraded installs heal themselves.
Internal
- ncip-server and digital-library declare their foreign keys / columns from a single source shared with the code that creates them, so the self-heal probe and the DDL cannot drift apart. New behavioural coverage (
tests/plugin-schema-selfheal-fkcol.unit.php) reproduces the missing-FK / missing-column state against the realPluginManagerand asserts it self-heals; it fails on the pre-fix code by design and is wired into the mandatory schema gate.
The ZIP was built twice with an identical SHA-256, audited as the
exact distributable, accompanied by an SPDX SBOM, and signed with
a GitHub artifact provenance attestation.
Pinakes v0.7.66
A full audit of all 20 bundled plugins — security, correctness and schema fixes — plus four previously-orphaned plugin features wired into the UI.
Security
- Centralised SSRF guard for caller-configurable endpoints:
HttpClientgains
anssrf_guardoption that resolves the target host, rejects hosts resolving to
private/reserved address space, pins the connection to the vetted public IP, and
rejects cross-host/port/scheme redirects. Enabled for the book-club AI endpoint
and the z39 SRU client, both of which also moved off rawcurlonto the guarded
client. - book-club AI settings are admin-only: the global API key and outbound
endpoint were reachable bystaff(AdminAuthMiddleware admits both roles); an
inline admin re-check now gates both read and save. - resource-sync Basic Auth is rate-limited; dewey-editor destructive
endpoints require admin inline; digital-library file-preview links get
rel="noopener".
Features
- api-book-scraper settings page is wired (
hasSettingsPage/
getSettingsViewPath) — previously a 404. - z39-server UNIMARC import completes the advertised round-trip: a pasted or
uploaded UNIMARC record pre-fills the book form (like the SBN import), never a
direct insert, so validation and soft-delete rules are preserved. - viaf-authority surfaces a VIAF/ISNI panel on the author edit page via the
existingauthor.form.fieldshook. - frbr-lrm wires Book↔Work linking and full Expression CRUD into the admin UI
viabook.form.fields; the manifest drops the unbuilt dedup/relator claims.
Fixed
- api-book-scraper no longer wipes its stored API key on a settings save that
leaves the key field blank (blank means "keep the existing key"). - archives soft-delete frees the UNIQUE
reference_code/ark_identifierfor
reuse by appending a per-id token (truncated to fitVARCHAR(64)), keeping the
original code as a readable prefix. - dewey-editor keys its data file off the full locale (
en_US≠en_GB) with
a legacy fallback; readers and writers share one resolver. - Plugin hook registration (api-book-scraper) is now transactional and rethrows on
failure; removed deadactivate()code in discogs/open-library; aligned
plugin.json versions and dropped stalemax_app_versionfields.
Upgrade
- The five plugins with schema or hook changes (ncip-server, digital-library,
frbr-lrm, viaf-authority, z39-server) get a plugin.json version bump so the
Updater re-runs theirensureSchema()/hook registration on existing installs —
the ncipncip_transactionsFOREIGN KEYs, digital-library'sfile_url/
audio_urlcolumns and the new frbr/viaf hooks are applied on upgrade, not only
on fresh install. No core SQL migration.
The ZIP was built twice with an identical SHA-256, audited as the
exact distributable, accompanied by an SPDX SBOM, and signed with
a GitHub artifact provenance attestation.
Pinakes v0.7.65
Add-to-calendar links in loan emails, the #366 legacy ready-pickup backfill, and a public-facing SEO pass.
Features
- Add-to-calendar links in loan emails: the loan-approved and pickup-ready
emails carry a calendar block with a Google Calendar link and a tokenized
per-loan.icsdownload, so borrowers can save the due date in any calendar
app (multi-calendar, no account linking). Rendered byNotificationService
viaLoanCalendarLinks; upgraded installs get the{{sezione_calendario}}
placeholder appended to already-seeded templates by an idempotent migration,
fresh installs from the template defaults. Strings translated in all five locales.
Fixed
- #366 residual repair: legacy
da_ritirarerows with a NULL
pickup_deadlinecould stay "ready for pickup" forever — the expiry sweep
skips NULL deadlines and the 0.7.63 repair intentionally preserves a pickup
whose copy is free.PickupDeadlineBackfillnow runs on every upgrade
(admin updater and Docker both driveUpdater::runMigrations()), after the
canonical triggers are re-applied, assigningtoday + pickup_expiry_days
capped at the loan'sdata_scadenza; rows whose window already closed get a
past deadline and are culled by the next sweep through the normal expiry
path. Covered bytests/migration-0.7.65-rc.1.unit.php.
SEO
- Canonical URLs no longer inherit tracking query strings (layout fallback and
book detail), so?utm_*variants stop becoming indexable duplicates. - hreflang alternates and locale-prefixed sitemap URLs (
/en/…) pointed to
non-routable 404s: hreflang is no longer emitted while the language is
session-based, and the sitemap lists only default-locale URLs. - The sitemap drops the auth pages (blocked by robots.txt anyway), skips
authors/publishers with no visible books, and logs when the explicit
book/author caps truncate; robots.txt now disallows every active locale's
login/register variant. - Author, publisher and genre archives share one localized SEO block:
canonical on the name-based URL (the id route collapses into it),
translated titles/descriptions, CollectionPage (+Person) and BreadcrumbList
JSON-LD. Book pages brand their title and Library schema with the
configuredapp.name. - Internal search results and the standalone auth pages carry
noindex,follow; emptyog:image/twitter:imagetags are omitted. - Catalog pagination is server-rendered with real
?page=Nlinks (crawlable
without JavaScript); grid covers load lazily and the book cover — the LCP —
getsfetchpriority="high". Book meta descriptions are cut multibyte-safe. - Public events emit
Event+BreadcrumbListJSON-LD.
The ZIP was built twice with an identical SHA-256, audited as the
exact distributable, accompanied by an SPDX SBOM, and signed with
a GitHub artifact provenance attestation.
Pinakes v0.7.64
Optional multiple physical copies of the same title per borrower (#238).
Features
- Multiple copies per borrower (opt-in, Settings → Loans → "Più copie dello
stesso titolo", off by default): staff can lend more than one distinct
physical copy of the same title to the same borrower. Every loan stays bound
to its own copy, so no two open loans for a borrower can share a copy.
Pending requests, reservations and legacy copyless rows keep the historical
borrower/title uniqueness, and per-copy overlap guards, eligibility, capacity
and the max-active-loans limit are all unchanged (each copy counts as one
active loan). The desk "Salva e registra un'altra copia" flow keeps the
selected title and focuses the next copy scan for fast batch check-outs.
Existing installations are unaffected without a migration — the setting
resolves to off until it is enabled, and fresh installs seed it off in all
five shipped locales.
Fixed
- Circulation can no longer hand out a copy that is physically out (#366
residual): creation, approval, rescheduling, renewal, reassignment and the DB
triggers all treat anin_corsoloan whose due date has passed but that the
maintenance sweep has not yet flipped toin_ritardoas an open-ended
commitment (same rule asCapacityService). The pre-assigned copy is
validated against the loan's book, and a request whose whole window is already
past is rejected instead of becoming an unpickableda_ritirare.
confirmPickupre-checks the loan rows holding the copy (not just
copie.stato) before issuing. Approval and the date-aware allocator still
accept a copy that is physically out as long as the requested window does not
overlap its open loan — the per-copy overlap check is the authority — so a
future-dated request is not needlessly blocked when the only copy is on loan. - Backdating a loan's due date reports a clear error: actively moving an
active loan's due date into the past used to reach the per-copy overlap
trigger and surface an opaqueloan_update_failed; the edit flow now rejects
it up front with the sameexpired_windowmessage the create/approve flows
use, while still allowing edits to an already-overdue loan whose due date is
left unchanged. - Queue promotion re-checks borrower eligibility: a user suspended (or
with an expired card) while waiting is skipped — their reservation stays
active in the queue — instead of being promoted to a pending loan that
approval then refuses, burning their position and pinning the copy. The scan
is not capped at the first 25 rows, so an eligible reader farther down the
FIFO can still receive a free copy. - Reservations on books without copy rows are refused up front: the queue
can only convert physicalcopierows, so a legacy book with none produced
reservations that silently never converted until they expired. - Reassigning a promoted loan keeps queue semantics (multiplicity ON): a
row withorigine='prenotazione'still inprenotato/da_ritirareis
treated as a queue commitment when reassigned to another borrower, so a user
can never end up with a physical loan plus a promoted queue position for the
same title. - The desk create form is double-submit safe: submit buttons disable on
first submit and every rendered form carries a one-time server token. A
replayed POST therefore cannot register a second real loan on another copy,
including retries caused by latency or double-clicks. - A freed copy now serves every compatible hold:
reassignOnReturnkeeps
assigning the returned copy until no blocked FIFO candidate can take it
(holds with disjoint windows previously waited for the next maintenance
sweep), and holds whose window is entirely past are no longer eligible. - Pickup-ready emails are claim-and-retry: a new
prestiti.pickup_notification_sentflag (seeded in the schema and added by
an idempotent upgrade migration) makes the "ready for pickup" email
idempotent. Token-owned claims recover after a worker crash, failures are
retried fairly by the hourly notification cron, and existing rows are
initialized as already handled without firing legacy circulation triggers.
Every future transition to ready resets the flag; reassigning a ready loan
resets the recipient-specific claim as well. - Borrowers without an email no longer wedge the notification crons:
expiry warnings and overdue notices for them keep their claim (no more
endless SMTP retry churn) and still produce the in-app/admin notifications,
which previously never fired for email-less borrowers. - Cron process locks no longer unlink after unlock (the race
scripts/maintenance.phpalready documented), both cron entrypoints refuse
non-CLI execution, andrunAll()refreshes the cross-session cooldown
marker so an admin login right after the cron no longer re-runs the whole
maintenance synchronously. - NCIP: a recognized active
FromAgencyIdis attributed to transaction
logs without silently turning the historically informational partner table
into a new authorization gate.CheckInItemandRenewItemreject a
malformedUserId, use a valid optionalUserIdto select the correct loan,
and refuse an ambiguous title-only mutation when several NCIP loans are open;
check-out/check-in/renew are logged toncip_transactions; renewals claim
the window from the day after the current due date (#336 parity with web). - Return flow: a deadlock/lock-timeout now reports a dedicated
"another operation was updating this book, retry" error instead of the
generic failure; reservation availability emails format dates in the
recipient's language like the rest of the #360 pipeline; cancelling a
promoted pending loan releases and reassigns its copy immediately. - Admin reservation edits: an out-of-whitelist status is rejected instead
of silently coerced toattiva, and a completed (promoted) reservation
cannot be flipped while its converted loan is still open. - Localized gender on the admin user-details page (#371): the stored
sessoenum (M/F/Altro) now renders through the same translated labels
as the edit form instead of the raw code, with a safe fallback for unexpected
values. - Notifications dropdown no longer clips on phones: the panel was anchored
to the right-hand bell button, so a near-full-width dropdown overflowed the
right screen edge and cut off "Segna tutte come lette" and the notification
bodies. Below themdbreakpoint it is now pinned to the viewport and
centred; the desktop panel is unchanged.
Changed
- Release artifacts now have one canonical producer: the tag-triggered
Verified Releaseworkflow builds twice, verifies the package, uploads a
draft, checks every server-side digest and only then publishes it. The local
release script performs preflight, pushes the annotated tag and monitors that
workflow instead of racing it with a second upload path. reassignOnNewCopynow walks the whole blocked-hold FIFO and assigns the
first compatible candidate (skipping an incompatible head) instead of
stopping at the first blocked hold; a same-borrower conflict on the target
copy blocks regardless of dates. Observable also with the multiplicity
setting off: a younger reservation can receive a returned copy while the
head stays blocked, without losing its priority for future copies.
The ZIP was built twice with an identical SHA-256, audited as the
exact distributable, accompanied by an SPDX SBOM, and signed with
a GitHub artifact provenance attestation.
Pinakes v0.7.63
Fixed
- #366 upgrade recovery:
migrate_0.7.63-rc.1.sqlimmediately demotes
impossible legacyda_ritirarerows back toprenotatowithout cancelling
the scheduled loan, clears their stale pickup deadline and releases the bad
copy assignment; maintenance repeats the same repair as a runtime safety net.
The loan-integrity trigger permits
corrective edits to an already-existing overdue overlap (including moving
the start date to tomorrow) while still rejecting newly introduced overlaps.
Scheduled loans without a pinned copy are assigned a real free copy before
being announced ready, soconfirmPickup()cannot inherit a copy-less row.
The ZIP was built twice with an identical SHA-256, audited as the
exact distributable, accompanied by an SPDX SBOM, and signed with
a GitHub artifact provenance attestation.