Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
2a4016e
fix(books): make the book-editor availability field read-only (#351)
fabiodalez-dev Aug 13, 2026
9e989f4
fix(books): stop the floating scroll-to-top button covering the Save …
fabiodalez-dev Aug 13, 2026
9a9fb41
feat(import): accept UPC-A barcodes (board games etc.) as EAN/UPC (#348)
fabiodalez-dev Aug 13, 2026
9ce61ed
fix(import): validate UPC-A before canonicalising; reject non-digit b…
fabiodalez-dev Aug 13, 2026
9cffc18
fix(import): strip only ASCII space/dash; don't truncate a formatted …
fabiodalez-dev Aug 13, 2026
8c9e2d4
refactor(import): keep UPC-A support to the single import path
fabiodalez-dev Aug 13, 2026
414d674
test(e2e): don't edit the read-only availability field in full-test 18.9
fabiodalez-dev Aug 13, 2026
696359c
Merge remote-tracking branch 'origin/main' into fix/issue-351-availab…
fabiodalez-dev Aug 13, 2026
666d1f4
Merge remote-tracking branch 'origin/main' into fix/upc-import-348
fabiodalez-dev Aug 13, 2026
82bb5ab
i18n: relabel the barcode field EAN → EAN/UPC (#348)
fabiodalez-dev Aug 13, 2026
e51e6a9
test(ncip): give CheckOut a guaranteed available copy (deterministic)
fabiodalez-dev Aug 13, 2026
b5a117e
test(ncip): give CheckOut a guaranteed available copy (deterministic)
fabiodalez-dev Aug 13, 2026
b691296
Merge remote-tracking branch 'origin/fix/issue-351-availability-reado…
fabiodalez-dev Aug 13, 2026
a6dcbbd
Merge remote-tracking branch 'origin/fix/upc-import-348' into release…
fabiodalez-dev Aug 13, 2026
01dc9ed
chore(release): 0.7.60
fabiodalez-dev Aug 13, 2026
e685f3e
test(ncip): robust dedicated-copy setup + README heading spacing
fabiodalez-dev Aug 13, 2026
78eb1e1
a11y(book-form): keep hidden scroll-to-top out of the tab order
fabiodalez-dev Aug 14, 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
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,35 @@

Full version-by-version history for Pinakes. The README shows only the latest release; everything older lives here.

## [0.7.60]

Maintenance release: UPC barcode support, a read-only availability field, and a
PHP 8.5 scraping fix, bundled with CI hardening.

### Features

- The barcode field accepts a 12-digit **UPC-A** (board games and other
non-book items), canonicalised to its 13-digit GTIN so it validates, stores,
searches and de-duplicates exactly like an EAN-13. CSV and TSV import both go
through the same path. The field is relabelled EAN → EAN/UPC across all five
locales.

### Fixes

- ISBN scraping no longer fails with "Risposta non valida dal servizio ISBN." on
PHP 8.5: the deprecated `curl_close()` calls that leaked a notice into the JSON
response body were removed.
- The book editor's availability field is now read-only, matching the fact that
it is a value derived from the physical copies; editing it was a silent no-op.
- The floating scroll-to-top button no longer overlaps the Save button at the
bottom of the book form.

### Internal

- The GitHub Actions security audit no longer breaks on upstream tag drift, and
the NCIP CheckOut regression test provisions its own available copy so it is
deterministic under sharded runs.

## [0.7.59]

Consolidation release: the complete integration of PRs #335, #337, #339 and
Expand Down
28 changes: 13 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,31 +39,29 @@ Pinakes is a self-hosted, full-featured ILS for schools, municipalities, and pri

## What's New

Highlights of the latest release are below. The full version-by-version history (v0.7.58 → v0.6.x) lives in **[CHANGELOG.md](CHANGELOG.md)**.
Highlights of the latest release are below. The full version-by-version history (v0.7.59 → v0.6.x) lives in **[CHANGELOG.md](CHANGELOG.md)**.

### v0.7.59 — latest
### v0.7.60 — latest

A consolidation release: four feature/fix branches integrated and hardened together, with a security pass on top.
A maintenance release: UPC barcode support, a read-only availability field, and a PHP 8.5 scraping fix.

### New
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- **"Complete series" indicator** — admins can mark a series as complete; the flag shows on the series list and detail pages (#338).

- **UPC-A barcodes are supported** — the barcode field now accepts a 12-digit UPC-A (board games and other non-book items) and stores it as its 13-digit GTIN, so it validates, searches and de-duplicates like an EAN-13. CSV and TSV import both handle it, and the field reads **EAN/UPC** in every locale (#348).

### Fixes
- **Loan editing from the admin page works again** — the availability re-check no longer bounces every edit, and dates are validated strictly (#336).
- **Loan status no longer shows "Unknown"** — cancelled and expired loans render their real state through canonical status/label helpers (#333).
- **The notifications panel is no longer overlapped** in the admin during scroll (#334).
- **Loan & reservation coherence** — clocks, availability and date handling share the same guarded paths across the web UI, mobile API and background jobs; auto-approval now honours the setting on the book-detail request path too (#301).
- **The barcode scanner keeps focus** on the loan form and copies sort in natural order (#238).
- **Related books are reachable on narrow and tablet screens** — the strip now shows a scroll affordance instead of silently clipping cards.

### Security
- Framework-generated error responses now receive the same nonce-based Content Security Policy as normal pages.
- **ISBN scraping works on PHP 8.5 again** — a deprecated `curl_close()` notice was leaking into the JSON response and breaking the import with "Risposta non valida dal servizio ISBN.".
- **The availability field in the book editor is now read-only** — it is derived from the physical copies, so editing it was a silent no-op (#351). Set a copy's status to make it unavailable.
- **The scroll-to-top button no longer covers the Save button** at the bottom of the book form.

### Internal

### Database Changes
- Adds a `collane.is_completa` flag; the in-app updater applies the migration automatically on upgrade.
- The GitHub Actions security audit no longer breaks on upstream action tag drift, and the NCIP CheckOut regression test is now deterministic under sharded runs.

### Upgrade Notes
- Back up your database before updating (the in-app updater does this automatically).

- No database changes. Back up your database before updating anyway (the in-app updater does this automatically).

> Older releases → **[CHANGELOG.md](CHANGELOG.md)**.

Expand Down
35 changes: 27 additions & 8 deletions app/Controllers/CsvImportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -1157,27 +1157,46 @@ private function normalizeIsbn(string $isbn): ?string
}

/**
* Validate and normalize EAN-13 barcode value
* Validate and normalize an EAN-13 or UPC-A barcode value
*
* Unlike normalizeIsbn(), this only validates format and length (13 digits)
* without ISBN checksum checks, since valid EAN-13 barcodes may not be ISBNs.
* Unlike normalizeIsbn(), this only validates format and length without
* ISBN checksum checks, since valid EAN-13 barcodes may not be ISBNs.
*
* @param string $ean Raw EAN value from CSV
* @return string|null Normalized EAN or null if invalid
* A UPC-A (GTIN-12, e.g. board games) is a GTIN-13/EAN-13 with a leading
* zero, and the zero-padding preserves its check digit (the EAN-13 weight
* pattern aligns once the leading zero occupies an odd position). So a
* 12-digit UPC-A is canonicalised to its 13-digit GTIN and then flows
* through the same EAN-13 validation, storage, dedup and search — the same
* barcode scanned as UPC-A or EAN-13 normalises to one value. (issue #348)
* Both CSV and TSV imports share this path (delimiter is auto-detected).
*
* @param string $ean Raw EAN/UPC value from CSV/TSV
* @return string|null Normalized 13-digit GTIN or null if invalid
*/
private function normalizeEan(string $ean): ?string
{
if (empty($ean)) {
return null;
}

// Remove all non-digit characters
$normalized = preg_replace('/[^0-9]/', '', trim($ean));
// Strip only the separators a barcode may carry: ASCII space and dash.
// Not \s and not trim() — those also remove TAB/CR/LF, which would let a
// field with a stray control character collapse into a "valid" barcode.
// Any character left after this means the cell is not a bare barcode
// (e.g. "ABC036000291452", "036000\n291452", "\t036000291452"), so
// reject it via the ctype_digit guard rather than extracting digits.
$normalized = str_replace([' ', '-'], '', $ean);

if (empty($normalized)) {
if ($normalized === '' || !ctype_digit($normalized)) {
return null;
}

// UPC-A (12 digits) is EAN-13 with a leading zero — canonicalise it so
// the shared EAN-13 checksum below validates it unchanged.
if (strlen($normalized) === 12) {
$normalized = '0' . $normalized;
}

// EAN-13 must be exactly 13 digits
if (strlen($normalized) !== 13) {
return null;
Expand Down
35 changes: 33 additions & 2 deletions app/Views/libri/partials/book_form.php
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@
<div>
<label for="stato" class="form-label"><?= __("Disponibilità") ?></label>
<?php $statoCorrente = strtolower((string) ($book['stato'] ?? '')); ?>
<select id="stato" name="stato" class="form-input">
<select id="stato" name="stato" class="form-input" disabled aria-readonly="true">
<option value="disponibile" <?php echo $statoCorrente === 'disponibile' ? 'selected' : ''; ?>><?= __("Disponibile") ?></option>
<option value="non_disponibile" <?php echo $statoCorrente === 'non_disponibile' ? 'selected' : ''; ?>><?= __("Non Disponibile") ?></option>
<option value="prestato" <?php echo $statoCorrente === 'prestato' ? 'selected' : ''; ?>><?= __("Prestato") ?></option>
Expand Down Expand Up @@ -1003,7 +1003,7 @@ class="w-4 h-4 rounded border-gray-300 text-gray-900 focus:ring-gray-500"
\App\Support\Hooks::do('book.form.fields', [$bookData, $bookId]);
?>

<div class="flex flex-col sm:flex-row gap-4 justify-end">
<div id="bookFormActions" class="flex flex-col sm:flex-row gap-4 justify-end">
<button type="button" id="btnCancel" class="btn-secondary order-2 sm:order-1">
<i class="fas fa-times mr-2"></i>
<?= __("Annulla") ?>
Expand All @@ -1016,6 +1016,37 @@ class="w-4 h-4 rounded border-gray-300 text-gray-900 focus:ring-gray-500"
</form>
</div>
</div>
<!--
The global "scroll to top" button is position:fixed in the bottom-right
corner; the form's Save/Cancel row is right-aligned at the end of the page.
Scrolled all the way down to save, the floating button lands on top of the
Save button. While the action row is in view (i.e. you're already at the
bottom), hide the floating button. The rule is !important so it overrides the
inline opacity the scroll-to-top partial sets on scroll; scoped to this page
via the body class, so no other page is affected. Degrades to the old
behaviour where IntersectionObserver is unavailable.
-->
<style>
body.book-actions-visible #scroll-to-top {
opacity: 0 !important;
pointer-events: none !important;
/* visibility:hidden also removes it from the tab order, so a keyboard user
can't Tab onto the invisible button while the Save/Cancel row is shown. */
visibility: hidden !important;
}
</style>
<script>
(function() {
var actions = document.getElementById('bookFormActions');
if (!actions || typeof IntersectionObserver === 'undefined') {
return;
}
var observer = new IntersectionObserver(function(entries) {
document.body.classList.toggle('book-actions-visible', entries[0].isIntersecting);
}, { rootMargin: '0px 0px -8px 0px' });
observer.observe(actions);
})();
</script>
<!-- CSS and JavaScript Libraries - partial-specific assets only (vendor.css/vendor.bundle.js loaded by layout) -->
<link rel="stylesheet" href="<?= htmlspecialchars(assetUrl('star-rating/dist/star-rating.min.css'), ENT_QUOTES, 'UTF-8') ?>">
<script src="<?= htmlspecialchars(assetUrl('star-rating/dist/star-rating.min.js'), ENT_QUOTES, 'UTF-8') ?>"></script>
Expand Down
6 changes: 3 additions & 3 deletions locale/da_DK.json
Original file line number Diff line number Diff line change
Expand Up @@ -1264,8 +1264,8 @@
"Dry-run (analizza senza inserire)": "Tørkørsel (analyser uden indsættelse)",
"Dry-run:": "Tørkørsel:",
"Durata": "Varighed",
"EAN": "EAN",
"EAN:": "EAN:",
"EAN": "EAN/UPC",
"EAN:": "EAN/UPC:",
"ERRORE:": "FEJL:",
"Eccellente": "Fremragende",
"Eccezione creazione %s su %s:": "Undtagelse ved oprettelse af %s på %s:",
Expand Down Expand Up @@ -1712,7 +1712,7 @@
"Etichetta \"ombrello\" per spin-off (es. tutto il franchise di Fairy Tail).": "\"Paraply\"-mærkat for spin-offs (f.eks. hele Fairy Tail-franchiset).",
"Etichette": "Etiketter",
"Etichette interne grandi (Herma 4630, Avery 3490)": "Store interne etiketter (Herma 4630, Avery 3490)",
"European Article Number (opzionale)": "European Article Number (valgfrit)",
"European Article Number (opzionale)": "European Article Number/Universal Product Code (valgfrit)",
"Eventi": "Begivenheder",
"Eventi Recenti": "Seneste begivenheder",
"Eventi e Incontri": "Begivenheder og møder",
Expand Down
6 changes: 3 additions & 3 deletions locale/de_DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -1264,8 +1264,8 @@
"Dry-run (analizza senza inserire)": "Testlauf (analysieren ohne einzufügen)",
"Dry-run:": "Testlauf:",
"Durata": "Dauer",
"EAN": "EAN",
"EAN:": "EAN:",
"EAN": "EAN/UPC",
"EAN:": "EAN/UPC:",
"ERRORE:": "FEHLER:",
"Eccellente": "Ausgezeichnet",
"Eccezione creazione %s su %s:": "Ausnahme beim Erstellen von %s auf %s:",
Expand Down Expand Up @@ -1712,7 +1712,7 @@
"Etichetta \"ombrello\" per spin-off (es. tutto il franchise di Fairy Tail).": "\"Dach\"-Bezeichnung für Spin-offs (z.B. das gesamte Fairy-Tail-Franchise).",
"Etichette": "Etiketten",
"Etichette interne grandi (Herma 4630, Avery 3490)": "Große interne Etiketten (Herma 4630, Avery 3490)",
"European Article Number (opzionale)": "European Article Number (optional)",
"European Article Number (opzionale)": "European Article Number/Universal Product Code (optional)",
"Eventi": "Veranstaltungen",
"Eventi Recenti": "Aktuelle Veranstaltungen",
"Eventi e Incontri": "Veranstaltungen und Treffen",
Expand Down
6 changes: 3 additions & 3 deletions locale/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1264,8 +1264,8 @@
"Dry-run (analizza senza inserire)": "Dry-run (parse without inserting)",
"Dry-run:": "Dry-run:",
"Durata": "Duration",
"EAN": "EAN",
"EAN:": "EAN:",
"EAN": "EAN/UPC",
"EAN:": "EAN/UPC:",
"ERRORE:": "ERROR:",
"Eccellente": "Excellent",
"Eccezione creazione %s su %s:": "Exception creating %s on %s:",
Expand Down Expand Up @@ -1712,7 +1712,7 @@
"Etichetta \"ombrello\" per spin-off (es. tutto il franchise di Fairy Tail).": "\"Umbrella\" label for spin-offs (e.g. the entire Fairy Tail franchise).",
"Etichette": "Labels",
"Etichette interne grandi (Herma 4630, Avery 3490)": "Large internal labels (Herma 4630, Avery 3490)",
"European Article Number (opzionale)": "European Article Number (optional)",
"European Article Number (opzionale)": "European Article Number/Universal Product Code (optional)",
"Eventi": "Events",
"Eventi Recenti": "Recent Events",
"Eventi e Incontri": "Events and Meetings",
Expand Down
6 changes: 3 additions & 3 deletions locale/fr_FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -1264,8 +1264,8 @@
"Dry-run (analizza senza inserire)": "Dry-run (analyser sans insérer)",
"Dry-run:": "Dry-run :",
"Durata": "Durée",
"EAN": "EAN",
"EAN:": "EAN :",
"EAN": "EAN/UPC",
"EAN:": "EAN/UPC :",
"ERRORE:": "ERREUR :",
"Eccellente": "Excellent",
"Eccezione creazione %s su %s:": "Exception de création de %s sur %s :",
Expand Down Expand Up @@ -1712,7 +1712,7 @@
"Etichetta \"ombrello\" per spin-off (es. tutto il franchise di Fairy Tail).": "Étiquette \"parapluie\" pour les spin-offs (ex. toute la franchise Fairy Tail).",
"Etichette": "Étiquettes",
"Etichette interne grandi (Herma 4630, Avery 3490)": "Grandes étiquettes internes (Herma 4630, Avery 3490)",
"European Article Number (opzionale)": "Numéro d'article européen (optionnel)",
"European Article Number (opzionale)": "Numéro d'article européen/Code universel des produits (optionnel)",
"Eventi": "Événements",
"Eventi Recenti": "Événements récents",
"Eventi e Incontri": "Événements et rencontres",
Expand Down
6 changes: 3 additions & 3 deletions locale/it_IT.json
Original file line number Diff line number Diff line change
Expand Up @@ -1264,8 +1264,8 @@
"Dry-run (analizza senza inserire)": "Dry-run (analizza senza inserire)",
"Dry-run:": "Dry-run:",
"Durata": "Durata",
"EAN": "EAN",
"EAN:": "EAN:",
"EAN": "EAN/UPC",
"EAN:": "EAN/UPC:",
"ERRORE:": "ERRORE:",
"Eccellente": "Eccellente",
"Eccezione creazione %s su %s:": "Eccezione creazione %s su %s:",
Expand Down Expand Up @@ -1712,7 +1712,7 @@
"Etichetta \"ombrello\" per spin-off (es. tutto il franchise di Fairy Tail).": "Etichetta \"ombrello\" per spin-off (es. tutto il franchise di Fairy Tail).",
"Etichette": "Etichette",
"Etichette interne grandi (Herma 4630, Avery 3490)": "Etichette interne grandi (Herma 4630, Avery 3490)",
"European Article Number (opzionale)": "European Article Number (opzionale)",
"European Article Number (opzionale)": "European Article Number/Universal Product Code (opzionale)",
"Eventi": "Eventi",
"Eventi Recenti": "Eventi Recenti",
"Eventi e Incontri": "Eventi e Incontri",
Expand Down
Loading