diff --git a/library/README.md b/library/README.md index ee959be..c074576 100644 --- a/library/README.md +++ b/library/README.md @@ -12,12 +12,19 @@ it never overwrites your `.env` (passwords) or your data. | What | Address | Login | |---|---|---| -| **Catalogue** (public search, no login) | `http://:8080` | none | -| **Staff interface** (cataloguing, labels, Z39.50) | `http://:8081` | `admin` and the generated password | -| **Add books** (scan an ISBN) | `http://:8082` | same login | +| **Your library**: add books, find a book, people, lending, labels, settings | `http://:8081` | `admin` and the generated password | +| **Add books** (scan an ISBN, or use the phone camera) | `http://:8081/add/` | same login (asked once more) | +| **Public catalogue** (search only, for a kiosk or visitors) | `http://:8080` | none | + +For home use you only need the first address: sign in to **Your library**, and use the **Add books** +button on its home page. The Add books page has a **Find a book** tab (title, author or ISBN, with the +shelf and whether it is on the shelf or checked out) and a **Manage** link back to the staff site. The +public catalogue exists for a kiosk screen or visitors and does not need a login. (Koha gives its public and +staff sites the same URL shapes, so they cannot share one address; Add books can, and does.) Ports move up to the next free number if one is taken; the final addresses are printed -at the end and by `sudo ./install.sh --status`. +at the end and by `sudo ./install.sh --status`. Add books is also reachable directly on its own port +(`8082`), which is handy behind a proxy but not needed otherwise. ## What it is made of diff --git a/library/docker-compose.yml b/library/docker-compose.yml index cdf0424..40dd3ed 100644 --- a/library/docker-compose.yml +++ b/library/docker-compose.yml @@ -30,8 +30,11 @@ services: depends_on: db: condition: service_healthy + quickadd: + condition: service_started environment: TZ: ${TZ:-UTC} + KOHA_QUICKADD_URL: http://quickadd:8082 KOHA_DB_HOST: db KOHA_DB_PASS: ${DB_PASSWORD} KOHA_ADMIN_USER: ${ADMIN_USER:-admin} @@ -53,9 +56,6 @@ services: image: library-quickadd:local container_name: library-quickadd restart: unless-stopped - depends_on: - koha: - condition: service_started environment: TZ: ${TZ:-UTC} KOHA_API: http://koha:8081/api/v1 diff --git a/library/install.sh b/library/install.sh index 60ce5ac..ebedaa2 100755 --- a/library/install.sh +++ b/library/install.sh @@ -79,10 +79,10 @@ show_status() { fi dc ps echo - echo " Catalogue (public): http://$ip:$OPAC_PORT" - echo " Staff interface: http://$ip:$STAFF_PORT" - echo " Add books (scan): http://$ip:${QUICKADD_PORT:-8082} (same login)" - echo " Staff login: $ADMIN_USER / $ADMIN_PASSWORD (also stored in $HERE/.env)" + echo " Your library: http://$ip:$STAFF_PORT" + echo " sign in here: add books at /add/ (or the 'Add books' button), find a book, lending, labels, people" + echo " Public catalogue: http://$ip:$OPAC_PORT (search only - for a kiosk or visitors)" + echo " Login: $ADMIN_USER / $ADMIN_PASSWORD (also stored in $HERE/.env; Add books asks once more with the same login)" echo } diff --git a/library/koha/bootstrap.pl b/library/koha/bootstrap.pl index d75b535..74f00b5 100755 --- a/library/koha/bootstrap.pl +++ b/library/koha/bootstrap.pl @@ -153,6 +153,19 @@ Koha::CirculationRules->set_rules({ }, }); +# ---- Staff home page: a "Library tools" block with a link to the Add books page ---- +# (Koha 26.05 stores these in the HTML customizations tool, not in a preference.) +{ + my $html = '

Library tools

'; + $dbh->do(q{INSERT INTO additional_contents (category, code, location, branchcode, published_on, number) + VALUES ('html_customizations', 'library_tools', 'IntranetmainUserblock', NULL, CURDATE(), 1)}); + my $id = $dbh->last_insert_id( undef, undef, 'additional_contents', 'id' ); + $dbh->do( q{INSERT INTO additional_contents_localizations (additional_content_id, title, content, lang) + VALUES (?, 'Library tools', ?, 'default')}, undef, $id, $html ); +} + # ---- Preferences ---------------------------------------------------------- my %prefs = ( autoBarcode => 'OFF', # barcodes are assigned by Quick Add (zero-padded, or none/scanned on request) diff --git a/library/koha/entrypoint.sh b/library/koha/entrypoint.sh index a99f0ef..34d5bf0 100755 --- a/library/koha/entrypoint.sh +++ b/library/koha/entrypoint.sh @@ -56,6 +56,20 @@ if [ ! -d "/etc/koha/sites/$INST" ]; then -e 's|^#\s*Include /etc/koha/apache-shared-intranet-plack.conf| Include /etc/koha/apache-shared-intranet-plack.conf|' "$SITE" koha-plack --enable "$INST" >/dev/null 2>&1 || true # koha-create may already have enabled it + # One address: serve the Add books page (Quick Add) inside the staff site at /add/. + # (The staff and public catalogue sites cannot share one address: Koha gives both the + # same URL shapes. The staff site is the natural home for a home library.) + if [ -n "${KOHA_QUICKADD_URL:-}" ]; then + cat > /etc/koha/apache-quickadd.conf << APACHE + + RedirectMatch ^/add\$ /add/ + ProxyPass /add/ ${KOHA_QUICKADD_URL%/}/ + ProxyPassReverse /add/ ${KOHA_QUICKADD_URL%/}/ + +APACHE + sed -i 's|^\(\s*\)Include /etc/koha/apache-shared-intranet.conf|&\n\1Include /etc/koha/apache-quickadd.conf|' "/etc/apache2/sites-available/$INST.conf" + fi + # Run Apache itself as the instance user instead of mpm_itk's per-vhost # uid switching (which needs extra capabilities and is pointless here: # Plack does the real work as that user already). diff --git a/library/quickadd/app.py b/library/quickadd/app.py index b885c18..ec2d1d7 100644 --- a/library/quickadd/app.py +++ b/library/quickadd/app.py @@ -687,6 +687,42 @@ def op_assign(payload): "last": fmt_barcode(first + len(items) - 1, s)} +# ------------------------------------------------------------------- search +def item_status(it): + if it.get("lost_status"): + return "Lost" + if it.get("withdrawn"): + return "Withdrawn" + if it.get("checked_out_date"): + return "Checked out" + if it.get("damaged_status"): + return "Damaged" + return "On shelf" + + +def op_search(term): + """Find books by title, author or ISBN; each result lists its copies with shelf and status.""" + term = re.sub(r"[%_\\]", "", term or "").strip()[:80] + if len(term) < 2: + return {"results": []} + like = {"-like": "%" + term + "%"} + q = {"-or": [{"title": like}, {"author": like}, {"isbn": like}, {"ean": like}]} + biblios = koha("GET", "/biblios", query={"q": json.dumps(q), "_per_page": "25", "_order_by": "title"}) or [] + if not biblios: + return {"results": []} + ids = [b["biblio_id"] for b in biblios] + items = koha("GET", "/items", query={"q": json.dumps({"biblio_id": ids}), "_per_page": "500"}) or [] + where = {l["value"]: l["label"] for l in get_locations()} + by_bib = {} + for it in items: + by_bib.setdefault(it["biblio_id"], []).append({ + "barcode": it.get("external_id"), "callnumber": it.get("callnumber") or "", + "location": where.get(it.get("location"), ""), "status": item_status(it)}) + return {"results": [{ + "biblio_id": b["biblio_id"], "title": (b.get("title") or "").rstrip(" /:"), "author": b.get("author") or "", + "year": b.get("copyright_date") or "", "copies": by_bib.get(b["biblio_id"], [])} for b in biblios]} + + # ------------------------------------------------------------ photo scanning MAX_IMAGE = 12 * 1024 * 1024 ZBAR_TIMEOUT = 20 @@ -801,6 +837,13 @@ class Handler(BaseHTTPRequestHandler): "sources": [s.split(":", 1)[0] if s.startswith("z3950") else s for s in SOURCES]}) if path == "/api/settings": return self._send(200, settings_view()) + if path == "/api/search": + qs = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query) + try: + return self._send(200, op_search((qs.get("q") or [""])[0])) + except KohaError as e: + return self._send(502, {"status": "error", "message": "Koha is not answering yet - try again in a minute." + if e.status == 503 else "Search failed: %s" % e.body[:120]}) self._send(404, {"error": "not found"}) def do_POST(self): diff --git a/library/quickadd/index.html b/library/quickadd/index.html index 4cb7127..db94b4f 100644 --- a/library/quickadd/index.html +++ b/library/quickadd/index.html @@ -44,6 +44,20 @@ padding: 10px 16px; background: var(--card); color: var(--ink); font-weight: 600; } button.primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); } button:focus-visible, a:focus-visible, input:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; } + nav.tabs { display: flex; flex-wrap: wrap; gap: 4px 6px; align-items: center; } + nav.tabs a { color: var(--accent); text-decoration: none; font-weight: 600; padding: 6px 12px; border-radius: 999px; } + nav.tabs a.on { background: var(--accent); color: var(--accent-ink); } + nav.tabs a.ext::after { content: ' \2197'; font-size: .85em; } + .find-form { display: flex; gap: 8px; margin: 10px 0; } + .find-form input { flex: 1; font-size: 1.3rem; padding: 12px 14px; border: 2px solid var(--line); border-radius: 12px; + background: var(--card); color: var(--ink); min-width: 0; } + .hit { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; margin: 10px 0; } + .hit .t { font-weight: 700; font-size: 1.05rem; overflow-wrap: anywhere; } + .hit a { color: var(--accent); } + .copy { display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center; padding: 6px 0 0; } + .chip { border-radius: 999px; padding: 1px 10px; font-size: .88rem; font-weight: 600; border: 1px solid var(--line); } + .chip.in { background: var(--ok-bg); border-color: var(--ok); color: var(--ok); } + .chip.out { background: var(--warn-bg); border-color: var(--warn); color: var(--warn); } .cam-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 8px 0 0; } #status { min-height: 1.5em; margin: 10px 2px; color: var(--muted); } .card { background: var(--card); border: 1px solid var(--line); border-radius: 14px; @@ -85,12 +99,26 @@
-

Add to library

-
+ + +
+
diff --git a/menus/addon_library.sh b/menus/addon_library.sh index fbe9b66..2c9c053 100644 --- a/menus/addon_library.sh +++ b/menus/addon_library.sh @@ -217,10 +217,10 @@ action_library_show_access() { ip="localhost" echo "(Network access is limited to this kiosk.)" fi - echo " Catalogue (public search): http://${ip}:$(library_env_get OPAC_PORT)/" - echo " Staff interface: http://${ip}:$(library_env_get STAFF_PORT)/" - echo " Add books (scan ISBNs): http://${ip}:$(library_env_get QUICKADD_PORT)/" - echo " Login (staff and add books): ${user} / ${pass}" + echo " Your library (sign in here): http://${ip}:$(library_env_get STAFF_PORT)/" + echo " Add books: http://${ip}:$(library_env_get STAFF_PORT)/add/" + echo " Public catalogue (kiosk): http://${ip}:$(library_env_get OPAC_PORT)/" + echo " Login: ${user} / ${pass} (Add books asks once more with the same login)" echo echo " Passwords live in $LIBRARY_DIR/.env (root only)." pause diff --git a/tests/addon_library.test.sh b/tests/addon_library.test.sh index b2f6a76..6f87f97 100755 --- a/tests/addon_library.test.sh +++ b/tests/addon_library.test.sh @@ -168,8 +168,9 @@ check ".env BIND_ADDR back to 127.0.0.1" test "$(library_env_get BIND_ADDR # ============================================================================ echo "== access screen" printf '\n' | action_library_show_access > "$T/access.out" 2>&1 -check "shows the staff login" grep -q 'admin / s3cretPASS' "$T/access.out" -check "shows quick-add address" grep -q 'Add books.*:8082' "$T/access.out" +check "shows the login" grep -q 'admin / s3cretPASS' "$T/access.out" +check "shows the one library address" grep -q 'sign in here.*:8081/' "$T/access.out" +check "shows the add-books path on that address" grep -q ':8081/add/' "$T/access.out" # ============================================================================ echo "== update"