diff --git a/library/README.md b/library/README.md index ef7a540..935ff98 100644 --- a/library/README.md +++ b/library/README.md @@ -12,9 +12,9 @@ it never overwrites your `.env` (passwords) or your data. | What | Address | 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 | +| **Your library**: add books, find a book, people, lending, labels, settings | `http://:8098` | `admin` and the generated password | +| **Add books** (scan an ISBN, or use the phone camera) | `http://:8098/add/` | same login (asked once more) | +| **Public catalogue** (search only, for a kiosk or visitors) | `http://:8097` | 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 @@ -24,7 +24,7 @@ staff sites the same URL shapes, so they cannot share one address; Add books can 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`. Add books is also reachable directly on its own port -(`8082`), which is handy behind a proxy but not needed otherwise. +(`8099`), which is handy behind a proxy but not needed otherwise. ## What it is made of @@ -290,3 +290,10 @@ points, quiz number and interest level fill in by themselves; check them and pre The book page's AR box has **Open beside this page** (a pop-up window next to the book) and **Show it here** (AR BookFind inside the page; some browsers block its cookies inside another page, then use the pop-up). Either copies the title first so you can paste it into AR's search; AR's own page cannot be pre-filled from here. + +### Same ports on every install + +New installs of this stack use the same three ports whether you install it from the kiosk add-on or from `ubuntu-post-install`: +**8098** is the one to use (the Room for Books home page, with Koha behind the menu), 8099 is Add books direct, and 8097 is the +public catalogue, which stays closed. If one is taken, the next free port is used and the installer prints it. Existing installs +keep the ports they already have. diff --git a/library/docker-compose.yml b/library/docker-compose.yml index 1153cd7..3ab7a64 100644 --- a/library/docker-compose.yml +++ b/library/docker-compose.yml @@ -46,8 +46,8 @@ services: KOHA_ITEM_TYPES: ${ITEM_TYPES:-} ports: # The public catalogue is off by default (loopback only); set OPAC_BIND=0.0.0.0 in .env to expose it. - - "${OPAC_BIND:-127.0.0.1}:${OPAC_PORT:-8080}:80" - - "${BIND_ADDR:-0.0.0.0}:${STAFF_PORT:-8081}:8081" + - "${OPAC_BIND:-127.0.0.1}:${OPAC_PORT:-8097}:80" + - "${BIND_ADDR:-0.0.0.0}:${STAFF_PORT:-8098}:8081" volumes: - koha-data:/var/lib/koha @@ -67,13 +67,13 @@ services: VIEWER_USER: ${VIEWER_USER:-guest} VIEWER_PASSWORD: ${VIEWER_PASSWORD:-} KOHA_LIBRARY: ${LIBRARY_CODE:-HOME} - OPAC_PORT: ${OPAC_PORT:-8080} - STAFF_PORT: ${STAFF_PORT:-8081} + OPAC_PORT: ${OPAC_PORT:-8097} + STAFF_PORT: ${STAFF_PORT:-8098} OPAC_URL: ${OPAC_URL:-} STAFF_URL: ${STAFF_URL:-} QUICKADD_SOURCES: ${QUICKADD_SOURCES:-z3950:lx2.loc.gov:210/LCDB,openlibrary} ports: - - "${BIND_ADDR:-0.0.0.0}:${QUICKADD_PORT:-8082}:8082" + - "${BIND_ADDR:-0.0.0.0}:${QUICKADD_PORT:-8099}:8082" volumes: - quickadd-data:/data diff --git a/library/install.sh b/library/install.sh index 0ecded4..67b4d6a 100755 --- a/library/install.sh +++ b/library/install.sh @@ -11,7 +11,7 @@ # LIBRARY_CODE=HOME short code for the library (letters/digits) # SHELVES="LR=Living room;BR=Bedroom" shelves/rooms to offer (first install only) # ITEM_TYPES="CD=Compact disc" extra item types (first install only) -# OPAC_PORT=8080 STAFF_PORT=8081 preferred host ports (next free is used) +# OPAC_PORT=8097 STAFF_PORT=8098 preferred host ports (next free is used) # EXTRA_CA=/path/to/proxy-ca.crt trust an extra CA while building set -euo pipefail @@ -117,9 +117,9 @@ LIBRARY_CODE=${LIBRARY_CODE:-HOME} SHELVES="${SHELVES:-}" ITEM_TYPES="${ITEM_TYPES:-}" TZ=${TZ_NOW} -OPAC_PORT=$(free_port "${OPAC_PORT:-8080}") -STAFF_PORT=$(free_port "${STAFF_PORT:-8081}") -QUICKADD_PORT=$(free_port "${QUICKADD_PORT:-8082}") +OPAC_PORT=$(free_port "${OPAC_PORT:-8097}") +STAFF_PORT=$(free_port "${STAFF_PORT:-8098}") +QUICKADD_PORT=$(free_port "${QUICKADD_PORT:-8099}") BIND_ADDR=${BIND_ADDR:-0.0.0.0} # The public catalogue is off by default: 127.0.0.1 = not reachable from the network. OPAC_BIND=${OPAC_BIND:-127.0.0.1} @@ -139,7 +139,7 @@ else c_info "Re-using existing .env" # Settings added in later versions: append them to an older .env. if ! grep -q '^QUICKADD_PORT=' .env; then - echo "QUICKADD_PORT=$(free_port 8082)" >> .env + echo "QUICKADD_PORT=$(free_port 8099)" >> .env fi if ! grep -q '^VIEWER_PASSWORD=' .env; then { echo "VIEWER_USER=guest"; echo "VIEWER_PASSWORD=$(rand 14)"; } >> .env @@ -170,7 +170,7 @@ with_retry dc up -d c_info "First start installs the database - waiting until Koha reports healthy..." for i in $(seq 1 180); do health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' library-koha 2>/dev/null || echo missing)" - if [ "$health" = healthy ] && curl -fsS -o /dev/null -m 5 "http://127.0.0.1:${QUICKADD_PORT:-8082}/healthz" 2>/dev/null; then READY=1; break; fi + if [ "$health" = healthy ] && curl -fsS -o /dev/null -m 5 "http://127.0.0.1:${QUICKADD_PORT:-8099}/healthz" 2>/dev/null; then READY=1; break; fi # One restart is tolerated (Docker's restart policy); a crash loop is not. if [ "$(docker inspect -f '{{.RestartCount}}' library-koha 2>/dev/null || echo 99)" -ge 3 ]; then c_err "library-koha keeps crashing - see: docker logs library-koha"; exit 1 diff --git a/library/quickadd/__pycache__/app.cpython-313.pyc b/library/quickadd/__pycache__/app.cpython-313.pyc index 687801f..9959cad 100644 Binary files a/library/quickadd/__pycache__/app.cpython-313.pyc and b/library/quickadd/__pycache__/app.cpython-313.pyc differ diff --git a/library/quickadd/app.py b/library/quickadd/app.py index 8694ab7..0571eca 100644 --- a/library/quickadd/app.py +++ b/library/quickadd/app.py @@ -985,7 +985,8 @@ def _loan_rows(cos): due = (c.get("due_date") or "")[:10] rows.append({"checkout_id": c["checkout_id"], "patron_id": c["patron_id"], "barcode": it.get("external_id") or "", "biblio_id": it.get("biblio_id"), "title": t[0], "author": t[1], "isbn": t[2], "due": due, - "overdue": bool(due) and due < today, "renewals": c.get("renewals_count") or 0}) + "overdue": bool(due) and due < today, "renewals": c.get("renewals_count") or 0, + "checkout_date": (c.get("checkout_date") or "")[:10]}) return rows diff --git a/library/quickadd/index.html b/library/quickadd/index.html index 1223f9b..0270da4 100644 --- a/library/quickadd/index.html +++ b/library/quickadd/index.html @@ -201,6 +201,7 @@
+
Check outScan a card, then the books ReturnScan the books coming back @@ -1243,21 +1244,37 @@ }; $('printcard').onclick = () => window.print(); - async function loadMine() { - let r; try { r = await api('/api/me'); } catch (e) { r = {}; } - if (!r.loans) { $('meloans').textContent = r.message || 'Could not load your books.'; return; } - $('mename').textContent = r.name; $('mecard').textContent = r.card; + // A borrower's loans: title, who wrote it, when it was borrowed, when it is due (days left / overdue), renewals, Renew. + function renderLoans(target, r, reload) { const today = new Date(); today.setHours(12, 0, 0, 0); const rows = r.loans.map(l => { const days = Math.round((new Date(l.due + 'T12:00:00') - today) / 86400000); const when = l.overdue ? 'Overdue by ' + (-days) + (days === -1 ? ' day' : ' days') : days === 0 ? 'Due today' : 'Due ' + fmtDue(l.due) + ' (in ' + days + (days === 1 ? ' day)' : ' days)'); const ren = el('button', { type: 'button' }, 'Renew'); ren.onclick = async () => { ren.disabled = true; const x = await api('/api/me/renew', { checkout_id: l.checkout_id }); - if (x.status === 'renewed') loadMine(); else { ren.disabled = false; alert(x.message || 'That one cannot be renewed.'); } }; - return el('div', { className: 'loan' }, el('span', { className: 't' }, el('a', { href: '#book/' + l.biblio_id }, l.title), el('div', { className: 'small' }, l.author)), - el('span', { className: 'chip ' + (l.overdue ? 'late' : 'in') }, when), ren); + if (x.status === 'renewed') reload(); else { ren.disabled = false; alert(x.message || 'That one cannot be renewed (the limit is reached, or someone is waiting for it).'); } }; + const facts = ['Borrowed ' + fmtDue(l.checkout_date), l.renewals ? 'renewed ' + (l.renewals === 1 ? 'once' : l.renewals + ' times') : ''].filter(Boolean).join(' · '); + return el('div', { className: 'loan' }, el('span', { className: 't' }, el('a', { href: '#book/' + l.biblio_id }, l.title), + el('div', { className: 'small' }, [l.author, facts].filter(Boolean).join(' · '))), + el('span', { className: 'chip ' + (l.overdue ? 'late' : days <= 3 ? 'out' : 'in') }, when), ren); }); - $('meloans').replaceChildren(...(rows.length ? rows : [el('div', { className: 'card ok' }, el('div', { className: 'body' }, 'You have nothing checked out right now.'))])); + const late = r.loans.filter(l => l.overdue).length; + const head = r.loans.length ? 'You have ' + r.loans.length + (r.loans.length === 1 ? ' book' : ' books') + ' out' + (late ? ', ' + late + ' overdue' : '') + '.' : 'You have nothing checked out right now.'; + target.replaceChildren(el('div', { className: 'card ' + (late ? 'warn' : 'ok') }, el('div', { className: 'body' }, el('strong', {}, head))), ...rows); + } + async function loadMine() { + let r; try { r = await api('/api/me'); } catch (e) { r = {}; } + if (!r.loans) { $('meloans').textContent = r.message || 'Could not load your books.'; return; } + $('mename').textContent = r.name; $('mecard').textContent = r.card; + renderLoans($('meloans'), r, loadMine); + } + async function loadHomeMine() { + let r; try { r = await api('/api/me'); } catch (e) { return; } + if (!r.loans) return; + const box = $('homemine'); box.hidden = false; + const inner = el('div', {}); + box.replaceChildren(el('h2', {}, 'Hello, ' + r.name), inner); + renderLoans(inner, r, loadHomeMine); } $('mepwform').onsubmit = async ev => { ev.preventDefault(); const f = ev.target; const r = await api('/api/me/password', Object.fromEntries(new FormData(f))); @@ -1324,7 +1341,7 @@ const names = { home: 'Home', library: 'Browse', book: 'Book', add: 'Add books', lend: 'Lend / return', people: 'People', card: 'Library card', cards: 'Library cards', mine: 'My books' }; document.title = (view === 'home' ? '' : names[view] + ' - ') + 'Room for Books'; window.scrollTo(0, 0); - if (view === 'home') { $('homeacts').hidden = viewer; $('patronacts').hidden = !isPatron; loadRecent(); if (h === 'find') $('hq').focus(); } + if (view === 'home') { $('homeacts').hidden = viewer; $('patronacts').hidden = !isPatron; $('homemine').hidden = !isPatron; if (isPatron) loadHomeMine(); loadRecent(); if (h === 'find') $('hq').focus(); } else if (view === 'book') loadBook(m[1]); else if (view === 'mine') loadMine(); else if (view === 'add') { if (h === 'settings') { $('settings').open = true; $('settings').scrollIntoView(); } else $('code').focus(); }