Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
5bd3963
CI: pin runners to ubuntu-24.04 and lift tinymce past GHSA-mf2p-h6hf-…
fabiodalez-dev Oct 27, 2026
5408876
Serve TinyMCE 8.10.0 from the committed copy
fabiodalez-dev Oct 28, 2026
7ea0755
Allowlist the 28 historic gitleaks findings that are examples
fabiodalez-dev Oct 28, 2026
e3cfed1
Keep the upstream TinyMCE changelog out of the copy and the release ZIP
fabiodalez-dev Oct 29, 2026
8ae13ae
Bump fast-uri from 3.1.8 to 3.1.9 in /frontend
dependabot[bot] Oct 30, 2026
0a7ee40
Merge main into CI/TinyMCE PR and preserve complete gitleaks allowlist
fabiodalez-dev Nov 3, 2026
4405882
Release 0.9.0-rc.3
fabiodalez-dev Nov 4, 2026
4e758c6
Fix calendar-aware circulation and add 25 independent settings scenarios
fabiodalez-dev Nov 4, 2026
96b4994
Merge current main into CI/TinyMCE PR
fabiodalez-dev Nov 4, 2026
842c1ae
Merge current main into fast-uri dependency update
fabiodalez-dev Nov 4, 2026
49064ef
Document calendar-aware renewals and native loan responses
fabiodalez-dev Nov 4, 2026
de425e5
Handle cookie consent in shared mobile circulation login
fabiodalez-dev Nov 4, 2026
38310a2
Include reviewed CI and TinyMCE history in release candidate
fabiodalez-dev Nov 4, 2026
85e00a3
Include reviewed fast-uri update in release candidate
fabiodalez-dev Nov 4, 2026
5681c4a
CI: dismiss cookie consent before mobile test login
fabiodalez-dev Nov 4, 2026
55f29aa
Include shared mobile test login fix for current CI
fabiodalez-dev Nov 4, 2026
ed3f662
Include shared mobile CI login fix history
fabiodalez-dev Nov 4, 2026
6a25da6
Include validated dependency branch CI prerequisite
fabiodalez-dev Nov 4, 2026
394eee9
Calendar-aware returns and renewals: review follow-ups
fabiodalez-dev Nov 4, 2026
133c840
Renewal refusal offers a date from the usable return day
fabiodalez-dev Nov 4, 2026
90c1b39
Bulk extension and due-soon push follow the usable return day
fabiodalez-dev Nov 4, 2026
6dc2406
Tests: the overdue-renewal check uses a loan that is really late
fabiodalez-dev Nov 4, 2026
5183260
Tests: a bulk extension's capacity window starts after the stored date
fabiodalez-dev Nov 4, 2026
c1849b3
Tests: H.25 checks the overdue refusal reason, dates in the app timezone
fabiodalez-dev Nov 4, 2026
d8a7b16
Serialize patron type changes and save assignments atomically
fabiodalez-dev Nov 4, 2026
ef2e7e2
Apply closure dates consistently across circulation channels
fabiodalez-dev Nov 4, 2026
5e34d7f
Strengthen notification coverage and isolate release test fixtures
fabiodalez-dev Nov 4, 2026
f5ad7f7
Count staff deadlines by their usable calendar day
fabiodalez-dev Nov 4, 2026
50068e8
Bound book detail memory and reject hidden PHP fatal errors
fabiodalez-dev Nov 4, 2026
99c3d20
Inspect label values independently of responsive captions and wrapping
fabiodalez-dev Nov 4, 2026
2103f14
Loans: every remaining surface shows the usable return day
fabiodalez-dev Nov 5, 2026
50454e5
Update symfony/html-sanitizer to 6.4.50 for CVE-2026-118517 and two r…
fabiodalez-dev Nov 5, 2026
bd26f18
Dashboard calendar keeps the stored loan status, only the end day fol…
fabiodalez-dev Nov 5, 2026
91540ca
Book page badge, dashboard overdue list and personal feed revision fo…
fabiodalez-dev Nov 5, 2026
3c58f12
Tests: the card photo check ignores the run id, login helpers wait br…
fabiodalez-dev Nov 5, 2026
8fa2250
Scheduled loans show the usable return day; the dashboard overdue lis…
fabiodalez-dev Nov 5, 2026
e036701
Personal feed keeps a booked loan until its usable return day
fabiodalez-dev Nov 5, 2026
86649e5
Close the remaining calendar gaps: library feed, book capacity, reque…
fabiodalez-dev Nov 5, 2026
2b2a405
The desk lists split the loans out by is_overdue; the request calenda…
fabiodalez-dev Nov 5, 2026
e40eac8
The request calendar marks the closed days of the branch holding the …
fabiodalez-dev Nov 5, 2026
347b0ef
Request end dates and the duration cap follow the branch of the book'…
fabiodalez-dev Nov 5, 2026
c2d46c1
A reservation turned into a loan is checked for capacity on the day t…
fabiodalez-dev Nov 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/ci-deep-regression.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ jobs:
sudo apt-get update -q
# php-apcu: the app's page cache must run on its production backend
# (APCu shared memory) under Apache/mod_php, not the file fallback.
sudo apt-get install -y apache2 libapache2-mod-php php-apcu default-mysql-client msmtp-mta
# PDF layout assertions need Poppler; without it they silently skip.
sudo apt-get install -y apache2 libapache2-mod-php php-apcu default-mysql-client msmtp-mta poppler-utils
sudo a2enmod rewrite headers env
php_module="$(find /etc/apache2/mods-available -maxdepth 1 -name 'php*.load' -printf '%f\n' | sed 's/\.load$//' | sort -V | tail -n1)"
[ -z "${php_module}" ] || sudo a2enmod "${php_module}"
Expand Down Expand Up @@ -232,6 +233,10 @@ jobs:
RESULT_FILE: test-results/shard-${{ matrix.shard }}.json
run: node scripts/ci-audit-playwright-results.js "$RESULT_FILE"

- name: Reject PHP fatal errors in the server log
if: always() && steps.deep-regression.outcome != 'skipped'
run: bash scripts/ci-check-php-errors.sh /var/log/apache2/pinakes-error.log

- name: Upload browser evidence
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
if: always()
Expand Down
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,38 @@

Full version-by-version history for Pinakes. The README shows only the last three releases; everything older lives here.

## [0.9.0-rc.3]

Release candidate: published as a GitHub prerelease, offered only to installations that opt in with `UPDATER_CHANNEL=rc` or `UPDATER_ALLOW_PRERELEASE=1`. No migration and no schema change.

### Loans follow closures added later

- **Existing due dates move.** Saving a closure, a weekly closed day or a holiday in Admin → Calendar now moves every active loan (out, waiting for pickup or booked ahead) whose due date, today or later, falls on a day that is now closed for the copy's branch to the next open day, as a loan made after the change would get; pickup deadlines follow. Each move is recorded in the loan history and re-arms the due-soon reminder. A move the book cannot take (the next reservation of that copy starts on the new date, or the added days are promised to another reservation) is left out and the loan keeps its date. A loan already marked late whose due date is now closed goes back to in progress. When a move is blocked by a lock (the book row held by another transaction) the calendar page warns that the moves are incomplete and the next save or the scheduled maintenance completes them; nothing is half-written.
- **Overdue counts start from the first open day.** A loan due on a day that was closed afterwards is not late until the first open day after it: the overdue sweep, the overdue e-mail, the mobile overdue push and the fine all follow `LibraryCalendar::isPastDue()`. Notices and recalls state the days late in open days, as the fine counts them, and never say "0 days" for a loan that is late.
- **Renewals, reader alerts and native clients agree with the calendar.** A reader can renew on the first usable return day after a retroactive closure, and the renewal counts from that day, as it would had the closure been saved before the due date. A return on that day is not recorded as late. Web lists, reader alerts, the mobile notification feed and the due-soon push use that same deadline. Reader and staff loan responses expose `effective_due_at` and `is_overdue`, while `due_at` retains the stored date for history and editing. Calendar saves also re-read capacity under the book lock, including reservations confirmed by another operator after the initial scan.
- **Recalls and pushes re-read the loan under the lock.** A manual recall or an overdue push claimed on stale data (a loan renewed or returned in the meantime) no longer goes out; the librarian is asked to reload the page, or told that another recall for the same loan has just gone out. A failed release of the calendar lock is retried at once, so the next batch does not wait for the session to end.
- **NCIP, reminder e-mails and exported calendars use the usable return day too.** NCIP renewals count from the same first open day as the desk, including when the old late flag has not been reconciled yet. Due-soon e-mails use that day for both the warning window and the message, checked again under the calendar lock. The library feed, the reader's personal feed and the per-loan calendar file keep an outstanding loan through that day and use its actual overdue status. The staff dashboard's "due today" count uses the same day, including dates whose move was blocked by capacity.
- **The remaining e-mails and staff pages use the usable return day too.** The overdue, recall, fine, receipt (with its PDF), approval, pickup and staff alert e-mails name the first open day after a closed due date, beside the open days counted from it. The admin dashboard, the loan desk, the staff tiles and lists, the reader's home page and the book page show that same day and mark a loan late only once it has gone by; a loan due today is no longer red on the book page, and a cancelled one no longer reads as overdue in its history. Saving a loan without changing its date no longer moves it or resets its reminders, and a renewal of a loan that is not yet late clears an old late flag.
- **Book availability follows the usable return day, by the library's choice.** A loan due on a closed day holds its book through the first open day after it, the day the reader is told, even when the move was refused and the row kept its date. Settings → Loans gains "Same-day handover after a closure": when on, another reader can start on that open day itself. It is off by default. A request's start day now also follows the branch of the book's copies when they all sit in one.
- **Concurrent circulation changes keep their commitments.** A calendar save re-reads the current copy and branch after taking the book lock. Reservations made eligible by a return under a per-material limit, or by a payment settling a debt, remain counted when the calendar checks whether a due date can move.
- **Patron types and accounts save consistently.** Changing the default type, editing it and deleting another type decide from locked rows, so a concurrent default change cannot erase the type's assignments or rules. Creating or editing a reader saves the account and its patron type together: a type removed during the request leaves neither a half-created account nor partially saved account fields.

### Large holdings

- **Book details fit within the default PHP memory limit.** A title with 9,999 physical copies can be opened without exhausting the 128 MiB limit. Rendering uses temporary streams instead of keeping several full HTML documents in memory. The creation regression now checks the complete detail response, and the deep CI gate rejects PHP fatal errors even when browser assertions pass.

### The 0.9.0 admin on a phone

- **Circulation settings.** The patron types table and the patron type × material rules matrix stack as cards below the `sm` breakpoint, with the same inputs; on desktop their labels no longer break mid-word.
- **Patron photo.** On a phone or tablet "Scatta foto" opens the device's own camera app (either lens); the shot is cropped to the card's 4:5 format and scaled to 480 × 600 with the EXIF orientation honoured. Desktops keep the inline webcam.
- **Visitor log.** The card field has the self-hosted camera scanner of the loan desk (Code128 and QR): a card read by the camera records the entry at once, a typed code still waits for "Registra ingresso".
- **Every 0.9.0 page checked at 390 px.** Attendance, closures calendar, stocktake, MARC import, suppliers, patron import, the new settings tabs, the reader record with its ledger and the return with a fine: nothing scrolls sideways, no field is small enough to make iOS zoom, wide tables read as cards with each value under its column name (as real text, for screen readers), the calendar's month arrows sit side by side and a notification count over 99 shows as "99+".

### Older admin pages on a phone

- **Book list, book page and book form.** The row actions stay inside the book list at full size and titles are no longer clamped; the book page's reservations, copies, volumes and loan history stack as cards; the change log wraps long values; the TinyMCE toolbar wraps on several rows; the chip remove cross is readable in both themes; a cover whose file is gone shows no empty frame and keeps "Rimuovi".
- **Long values wrap.** Write-permission paths in Admin → Updates, catalogue URLs in copy cataloguing and the linked records of an archival authority no longer run past the screen edge.

## [0.9.0-rc.2]

Release candidate: published as a GitHub prerelease, offered only to installations that opt in with `UPDATER_CHANNEL=rc` or `UPDATER_ALLOW_PRERELEASE=1`. No migration: the only schema change is in the Mobile API plugin, which widens `mobile_import_jobs.kind` from an ENUM to `VARCHAR(32)` in its own `ensureSchema()` when it upgrades to 1.10.0.
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ Pinakes is a self-hosted, full-featured ILS for schools, municipalities, and pri
[![Documentation](https://img.shields.io/badge/Documentazione-Docsify-4285f4?style=for-the-badge&logo=readthedocs&logoColor=white)](https://fabiodalez-dev.github.io/Pinakes/)
[![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://buymeacoffee.com/fabiodalez)

## What's New in v0.9.0-rc.2
## What's New in v0.9.0-rc.3

**Release candidate. Everything the 0.9.0 admin does, ready for the Android app.** The Mobile API plugin (1.10.0) opens every new 0.9.0 tool to the staff app: stocktake with the phone as scanner, the patron ledger at the desk, library settings, patron types, the rules matrix and the closures calendar, suppliers, catalogue cards, MARC import and export, copy cataloguing, the visitor log, statistics and patron import from CSV. Each area follows its module switch and goes through the same controllers as the website; the Android app gains the matching screens in its next release. No migration. Full details in **[CHANGELOG.md](CHANGELOG.md)**.
**Release candidate. Loans follow closures added later, and the admin works on a phone.** Saving a closure, a weekly closed day or a holiday now moves the active loans that fall on a newly closed day to the next open day, with their pickup deadlines, history and due-soon reminder; a loan due on a day closed afterwards is not late until the first open day after it, and the overdue sweep, e-mail, push and fine all count open days only. Every 0.9.0 page and the older book list, book page, book form, updates and archival pages were checked at 390 px: nothing scrolls sideways, wide tables read as cards and long values wrap. No migration. Full details in **[CHANGELOG.md](CHANGELOG.md)**.

---

Expand All @@ -53,13 +53,13 @@ The showcase contains 50 catalogue books, 8 desiderata, 3 periodicals with 10 is

Highlights of the two releases before the current one are below; the README never lists more than the last three versions. The full version-by-version history lives in **[CHANGELOG.md](CHANGELOG.md)**.

### v0.9.0-rc.1
### v0.9.0-rc.2

**Stocktake, MARC in and out, and the circulation tools of a full library system.** Inventory stocktake sessions with barcode, camera or list scanning and a live report; MARC 21, UNIMARC and danMARC2 bulk import and export with copy cataloguing over SRU; a visitor log, ISBD catalogue cards, suppliers and RDA types; printable library cards, patron photos and user import from CSV; patron types with a rules matrix, a closures calendar and optional fines with a patron ledger. Every module is optional and starts switched off on an upgrade. One additive migration (`migrate_0.9.0-rc.1.sql`). Full details in **[CHANGELOG.md](CHANGELOG.md)**.
**Everything the 0.9.0 admin does, ready for the Android app.** The Mobile API plugin (1.10.0) opens every new 0.9.0 tool to the staff app: stocktake with the phone as scanner, the patron ledger at the desk, library settings, patron types, the rules matrix, the closures calendar, suppliers, catalogue cards, MARC import and export, copy cataloguing, the visitor log, statistics and patron import, each through the same controllers as the website. No migration. Full details in **[CHANGELOG.md](CHANGELOG.md)**.

### v0.8.9
### v0.9.0-rc.1

**Search Console verification, a pickup reminder, and an SEO pass.** Settings → Advanced has a field for the `<meta>` verification tags of Google, Bing and the other search engines ([#497](https://github.com/fabiodalez-dev/Pinakes/pull/497)). Readers get a reminder before the pickup deadline passes, "Loan approved" can say how many days they have to pick the book up, and the staff is told when a pickup expires ([#498](https://github.com/fabiodalez-dev/Pinakes/pull/498)); the new approval text ships to new installations only, existing ones keep their template and can add `{{giorni_ritiro}}` and `{{scadenza_ritiro}}`. The 404 page is no longer indexed, empty pages past the last one answer 404, structured data and the sitemap are more complete, and the loan request calendar fills its row. One additive migration (`migrate_0.8.9.sql`). Full details in **[CHANGELOG.md](CHANGELOG.md)**.
**Stocktake, MARC in and out, and the circulation tools of a full library system.** Inventory stocktake sessions with barcode, camera or list scanning and a live report; MARC 21, UNIMARC and danMARC2 bulk import and export with copy cataloguing over SRU; a visitor log, ISBD catalogue cards, suppliers and RDA types; printable library cards, patron photos and user import from CSV; patron types with a rules matrix, a closures calendar and optional fines with a patron ledger. Every module is optional and starts switched off on an upgrade. One additive migration (`migrate_0.9.0-rc.1.sql`). Full details in **[CHANGELOG.md](CHANGELOG.md)**.

Older releases are in **[CHANGELOG.md](CHANGELOG.md)**.

Expand Down
1 change: 1 addition & 0 deletions app/Controllers/Admin/LibraryCalendarController.php
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,7 @@ private function commitChange(\mysqli $db, callable $write): ?string
LibraryCalendar::reset();
LibraryCalendar::extendDueDates($db);
LibraryCalendar::extendPickupDeadlines($db);
LibraryCalendar::bumpRevision($db);
$db->commit();
return null;
}
Expand Down
36 changes: 36 additions & 0 deletions app/Controllers/CirculationSettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,15 @@ public function updateType(Request $request, Response $response, mysqli $db, int
}
try {
$db->begin_transaction();
$locked = $this->lockType($db, $id);
if ($locked === null) {
$db->rollback();
$_SESSION['error_message'] = __('Tipologia lettore non trovata.');
return $response->withHeader('Location', url(self::TAB_URL))->withStatus(302);
}
if ((int) $locked['predefinita'] === 1) {
$fields['attiva'] = 1;
}
$this->update($db, 'tipologie_lettore', $fields, $id);
if (!empty($data['predefinita'])) {
$this->setDefaultTx($db, $id);
Expand Down Expand Up @@ -289,6 +298,17 @@ public function deleteType(Request $request, Response $response, mysqli $db, int
} else {
try {
$db->begin_transaction();
// The initial form read may precede another administrator's
// set-default transaction. Decide before touching readers or
// rules, from the same locked row setDefaultTx() uses.
$locked = $this->lockType($db, $id);
if ($locked === null || (int) $locked['predefinita'] === 1) {
$db->rollback();
$_SESSION['error_message'] = $locked === null
? __('Tipologia lettore non trovata.')
: __('La tipologia predefinita non può essere eliminata: scegli prima un\'altra predefinita.');
return $response->withHeader('Location', url(self::TAB_URL))->withStatus(302);
}
// Patrons of the removed type fall back to the default one
// (the FK is ON DELETE SET NULL; done explicitly for clarity).
$stmt = $db->prepare('UPDATE utenti SET tipologia_id = NULL WHERE tipologia_id = ?');
Expand Down Expand Up @@ -603,13 +623,29 @@ private function codeTaken(mysqli $db, string $code, ?int $exceptId): bool
/** Exactly one default type: clear the flag everywhere, then set it (inside the caller's transaction). */
private function setDefaultTx(mysqli $db, int $id): void
{
// A target deleted after the page read must not clear the old default.
// Lock before either write so deletion and default selection serialize.
if ($this->lockType($db, $id) === null) {
throw new \RuntimeException('Patron type no longer exists.');
}
$db->query('UPDATE tipologie_lettore SET predefinita = 0 WHERE predefinita = 1');
$stmt = $db->prepare('UPDATE tipologie_lettore SET predefinita = 1, attiva = 1 WHERE id = ?');
$stmt->bind_param('i', $id);
$stmt->execute();
$stmt->close();
}

/** Current patron type inside the caller's transaction. @return array<string, mixed>|null */
private function lockType(mysqli $db, int $id): ?array
{
$stmt = $db->prepare('SELECT * FROM tipologie_lettore WHERE id = ? FOR UPDATE');
$stmt->bind_param('i', $id);
$stmt->execute();
$row = $stmt->get_result()->fetch_assoc();
$stmt->close();
return $row ?: null;
}

/** @param array<string, int|string|null> $fields */
private function insert(mysqli $db, string $table, array $fields): int
{
Expand Down
Loading