From ec9aa605cf4e87c67b84427aa9ab0297de9ab433 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 18:39:27 +0000 Subject: [PATCH 1/2] Fix named volumes in koha/asterisk for backup coverage; add Koha setup wizard koha.sh: - Replace named volume `koha_db_data` with bind-mount `./data:/var/lib/mysql` so the MariaDB database is inside ~/docker/koha/ and covered by backup.sh - Add interactive setup wizard: collects library name, code, admin credentials, item types (with custom additions), and shelf locations before first start - Generate ~/docker/koha/post-setup.sh at install time; after completing the web installer the user runs it to auto-configure the library branch, item types, shelf locations, and system preferences via the Koha REST API - Updated README with exact step-by-step web installer instructions and admin password prominently displayed asterisk.sh: - Replace all 5 named volumes (asterisk-config, easy-asterisk-config, asterisk-logs, asterisk-spool, asterisk-lib) with bind-mounts inside ~/docker/asterisk/ so all config and state is covered by backup.sh - mkdir -p for all bind-mount dirs before compose up - Replace SSH-based standalone configure_caddy_for_service stub with the snippet approach used everywhere else (local Caddy writes Caddyfile; remote Caddy via VPN/Netbird saves ~/docker/caddy-snippets/.caddy) https://claude.ai/code/session_01S7UecmQRG6CKTYPoBqbVLj --- services/asterisk.sh | 191 +++++++++---------- services/koha.sh | 443 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 475 insertions(+), 159 deletions(-) diff --git a/services/asterisk.sh b/services/asterisk.sh index 74498f4..85ae9c4 100644 --- a/services/asterisk.sh +++ b/services/asterisk.sh @@ -63,28 +63,49 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}" local _caddy_dir="$DOCKER_DIR/caddy" local _caddyfile="$_caddy_dir/Caddyfile" + local _display_port="${_upstream##*:}" - # Remote Caddy support: if CADDY_REMOTE_HOST is set, operate on the - # remote machine via SSH instead of the local filesystem. - if [[ -n "${CADDY_REMOTE_HOST:-}" ]]; then - echo "" - local _do_caddy="" - read -r -p " Configure Caddy reverse proxy for $_name on $CADDY_REMOTE_HOST? [y/N]: " _do_caddy - [[ "${_do_caddy,,}" == "y" ]] || { - log_info "Skipping — access at: http://$(hostname -I | awk '{print $1}'):${_upstream##*:}" - return 0 - } + local _mode="none" + [[ -d "$_caddy_dir" ]] && _mode="local" + [[ -n "${CADDY_REMOTE_HOST:-}" ]] && [[ "$_mode" != "local" ]] && _mode="remote" + [[ "$_mode" == "none" ]] && { + log_info "Access $_name directly on port $_display_port." + return 0 + } - local _domain="" - read -r -p " Domain (e.g. ${_subdomain}.${SITE_DOMAIN:-example.com}): " _domain - [[ -n "$_domain" ]] || { log_warning "No domain entered — skipping Caddy."; return 0; } + echo "" + local _do_caddy="" + if [[ "$_mode" == "remote" ]]; then + log_info "Remote Caddy configured (${CADDY_REMOTE_HOST})." + log_info "A snippet file will be saved to ~/docker/caddy-snippets/." + fi + read -r -p " Configure Caddy reverse proxy for $_name? [y/N]: " _do_caddy + [[ "${_do_caddy,,}" == "y" ]] || { + log_info "Skipping — access at: http://localhost:$_display_port" + return 0 + } - local _block - _block="$(cat << CBLOCK + local _default_domain="" + if [[ -n "${SITE_DOMAIN:-}" ]] && [[ "$SITE_DOMAIN" != "example.com" ]]; then + _default_domain="${_subdomain}.${SITE_DOMAIN}" + log_info "Default: $_default_domain" + fi + local _domain="" + read -r -p " Domain [${_default_domain:-required}]: " _domain + _domain="${_domain:-$_default_domain}" + [[ -n "$_domain" ]] || { log_warning "No domain entered — skipping Caddy."; return 0; } + + local _block_upstream="$_upstream" + if [[ "$_mode" == "remote" ]]; then + _block_upstream="${CADDY_REMOTE_HOST}:${_display_port}" + fi + + local _site_block + _site_block="$(cat << CBLOCK # $_name -$_domain { - reverse_proxy $_upstream +${_domain} { + reverse_proxy ${_block_upstream} header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" @@ -101,79 +122,43 @@ ${_extra} } CBLOCK )" - echo "$_block" | ssh "$CADDY_REMOTE_HOST" "cat >> $_caddyfile" - ssh "$CADDY_REMOTE_HOST" "docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true" - if ssh "$CADDY_REMOTE_HOST" "docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null"; then + + if [[ "$_mode" == "local" ]]; then + if [[ -f "$_caddyfile" ]]; then + local _bk="$_caddy_dir/Caddyfile.backup.$(date +%Y%m%d-%H%M%S)" + cp "$_caddyfile" "$_bk" + log_info "Backed up Caddyfile to $(basename "$_bk")" + else + touch "$_caddyfile" + fi + + if grep -q "^${_domain}" "$_caddyfile" 2>/dev/null; then + log_warning "$_domain already in Caddyfile" + local _ow="" + read -r -p " Overwrite? [y/N]: " _ow + [[ "${_ow,,}" == "y" ]] || { log_info "Keeping existing entry."; return 0; } + sed -i "/^${_domain}/,/^}/d" "$_caddyfile" + fi + + printf '%s\n' "$_site_block" >> "$_caddyfile" + log_success "Added $_domain to Caddyfile" + docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true + if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then log_success "$_name accessible at: https://$_domain" else - log_warning "Reload failed — check: ssh $CADDY_REMOTE_HOST docker logs caddy" + log_warning "Reload failed — check: docker logs caddy" + log_info "Manual reload: docker exec caddy caddy reload --config /etc/caddy/Caddyfile" fi - return 0 - fi - - if [[ ! -d "$_caddy_dir" ]]; then - log_info "Access $_name directly on port ${_upstream##*:}." - return 0 - fi - - echo "" - local _do_caddy="" - read -r -p " Configure Caddy reverse proxy for $_name? [y/N]: " _do_caddy - [[ "${_do_caddy,,}" == "y" ]] || { - log_info "Skipping — access at: http://localhost:${_upstream##*:}" - return 0 - } - - local _domain="" - read -r -p " Domain (e.g. ${_subdomain}.${SITE_DOMAIN:-example.com}): " _domain - [[ -n "$_domain" ]] || { log_warning "No domain entered — skipping Caddy."; return 0; } - - # Back up before touching - if [[ -f "$_caddyfile" ]]; then - local _bk="$_caddy_dir/Caddyfile.backup.$(date +%Y%m%d-%H%M%S)" - cp "$_caddyfile" "$_bk" - log_info "Backed up Caddyfile to $(basename "$_bk")" else - touch "$_caddyfile" - fi - - # Remove existing block for this domain if present - if grep -q "^${_domain}" "$_caddyfile" 2>/dev/null; then - log_warning "$_domain already in Caddyfile" - local _ow="" - read -r -p " Overwrite? [y/N]: " _ow - [[ "${_ow,,}" == "y" ]] || { log_info "Keeping existing entry."; return 0; } - sed -i "/^${_domain}/,/^}/d" "$_caddyfile" - fi - - cat >> "$_caddyfile" << CBLOCK - -# $_name -$_domain { - reverse_proxy $_upstream - - header { - Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" - X-Content-Type-Options "nosniff" - X-Frame-Options "SAMEORIGIN" - Referrer-Policy "strict-origin-when-cross-origin" - } - - log { - output file /var/log/caddy/${_domain}.log - format json - } -${_extra} -} -CBLOCK - - log_success "Added $_domain to Caddyfile" - docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true - if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then - log_success "$_name accessible at: https://$_domain" - else - log_warning "Reload failed — check: docker logs caddy" - log_info "Manual reload: docker exec caddy caddy reload --config /etc/caddy/Caddyfile" + local _snippet_dir="$DOCKER_DIR/caddy-snippets" + local _snippet_file="$_snippet_dir/${_subdomain}.caddy" + mkdir -p "$_snippet_dir" + printf '%s\n' "$_site_block" > "$_snippet_file" + chown "$ACTUAL_USER:$ACTUAL_USER" "$_snippet_file" 2>/dev/null || true + log_success "Snippet saved: $_snippet_file" + log_info "Copy to Caddy machine:" + log_info " scp $_snippet_file caddy-host:~/caddy-snippets/" + log_info " rsync -av $_snippet_dir/ caddy-host:~/caddy-snippets/ (all at once)" fi } @@ -224,6 +209,8 @@ install_asterisk() { fi mkdir -p "$EA_DIR" + mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \ + "$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" ensure_docker_dir_ownership "$EA_DIR" cd "$EA_DIR" || return 1 @@ -287,11 +274,11 @@ services: coturn: condition: service_started volumes: - - asterisk-config:/etc/asterisk - - easy-asterisk-config:/etc/easy-asterisk - - asterisk-logs:/var/log/asterisk - - asterisk-spool:/var/spool/asterisk - - asterisk-lib:/var/lib/asterisk + - ./config/asterisk:/etc/asterisk + - ./config/easy-asterisk:/etc/easy-asterisk + - ./logs:/var/log/asterisk + - ./spool:/var/spool/asterisk + - ./lib:/var/lib/asterisk - ./easy-asterisk.sh:/usr/local/bin/easy-asterisk:ro env_file: .env restart: unless-stopped @@ -327,12 +314,6 @@ services: - --log-file=stdout restart: unless-stopped -volumes: - asterisk-config: - easy-asterisk-config: - asterisk-logs: - asterisk-spool: - asterisk-lib: EOF # ── .env ────────────────────────────────────────────────────────────────── @@ -428,15 +409,15 @@ Recommended softphones: Linphone, Zoiper, Bria, Grandstream Wave. Access the Easy Asterisk web interface at http://:8080 or via your configured reverse-proxy domain. -## Volumes +## Data directories (all inside ~/docker/asterisk/, included in backup) -| Volume | Contents | -|----------------------|-------------------------------| -| asterisk-config | /etc/asterisk — dialplan, SIP | -| easy-asterisk-config | /etc/easy-asterisk — web config| -| asterisk-logs | /var/log/asterisk | -| asterisk-spool | /var/spool/asterisk | -| asterisk-lib | /var/lib/asterisk | +| Directory | Contents | +|----------------------|---------------------------------| +| config/asterisk/ | /etc/asterisk — dialplan, SIP | +| config/easy-asterisk/| /etc/easy-asterisk — web config | +| logs/ | /var/log/asterisk | +| spool/ | /var/spool/asterisk | +| lib/ | /var/lib/asterisk | ## Ports diff --git a/services/koha.sh b/services/koha.sh index 4915a10..33accc4 100644 --- a/services/koha.sh +++ b/services/koha.sh @@ -10,6 +10,8 @@ # background jobs worker) + MariaDB + Memcached + RabbitMQ sidecars. # OPAC (patron UI): port 8097 | Staff/admin: port 8098 # RAM: needs ~2.5 GB free. +# +# Database lives in ~/docker/koha/data/ (bind-mount, fully backed up by backup.sh). # ── Standalone bootstrap ────────────────────────────────────────────────────── if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then @@ -165,6 +167,7 @@ CBLOCK log_info " rsync -av $_snippet_dir/ caddy-host:~/caddy-snippets/ (all at once)" fi } + write_readme() { local _dir="$1"; shift mkdir -p "$_dir" @@ -194,33 +197,156 @@ install_koha() { local KOHA_DIR="$DOCKER_DIR/koha" + echo "" + echo "╔═══════════════════════════════════════════════════════════════════╗" + echo "║ Koha — Integrated Library System ║" + echo "╚═══════════════════════════════════════════════════════════════════╝" + echo "" + echo " • ISBN barcode scanning → auto-fetch cover art, author, summary" + echo " • Shelf / location tracking (define your own codes)" + echo " • Loan / checkout system with due dates and history" + echo " • OPAC (patron browsing UI) + staff admin interface" + echo " • Requires ~2.5 GB free RAM" + echo "" + echo " SETUP OVERVIEW:" + echo " 1) Answer the questions below (collects library details + credentials)" + echo " 2) Koha starts — takes 2–3 min on first boot" + echo " 3) Open http://localhost:8098 and complete the brief web installer (~2 min)" + echo " 4) Run $KOHA_DIR/post-setup.sh to auto-configure library, items, locations" + echo "" + if [ "$DRY_RUN" = true ]; then - echo "[DRY-RUN] koha would:" - echo " - Create $KOHA_DIR with docker-compose.yml + config-main.env" - echo " - Deploy teogramm/koha + MariaDB + Memcached + RabbitMQ" - echo " - OPAC (patron UI): port 8097" - echo " - Staff/admin: port 8098" - echo " - Requires ~2.5 GB free RAM" + echo "[DRY-RUN] Would create $KOHA_DIR with docker-compose.yml + config-main.env" + echo "[DRY-RUN] Would deploy teogramm/koha + MariaDB (./data/) + Memcached + RabbitMQ" + echo "[DRY-RUN] Would generate post-setup.sh for REST API configuration" + echo "[DRY-RUN] OPAC (patron UI): port 8097" + echo "[DRY-RUN] Staff/admin: port 8098" return 0 fi - echo "" - echo " Koha is a full Integrated Library System (ILS):" - echo " • ISBN barcode scanning → auto-fetch cover art, author, summary" - echo " • Shelf / location tracking (define your own locations)" - echo " • Loan / checkout system with due dates and history" - echo " • OPAC (patron browsing UI) + staff admin interface" - echo " • Requires ~2.5 GB free RAM (4 containers: Koha, MariaDB, Memcached, RabbitMQ)" + # ── Collect library details ─────────────────────────────────────────────── + echo "══════════════════════════════════════════════════════" + echo " LIBRARY DETAILS" + echo "══════════════════════════════════════════════════════" echo "" - local DB_PASS RABBIT_PASS - DB_PASS=$(generate_password 24) - RABBIT_PASS=$(generate_password 24) + local LIB_NAME="" + prompt_text "Library name (shown in OPAC header) [My Home Library]:" "My Home Library" LIB_NAME - mkdir -p "$KOHA_DIR" + # Auto-derive a library code from the name + local _auto_code + _auto_code="$(echo "$LIB_NAME" | tr '[:lower:]' '[:upper:]' | tr -dc 'A-Z0-9' | head -c 5)" + [[ -z "$_auto_code" ]] && _auto_code="HOME" + local LIB_CODE="" + prompt_text "Library code (3-8 letters/numbers) [${_auto_code}]:" "$_auto_code" LIB_CODE + LIB_CODE="${LIB_CODE//[^A-Za-z0-9]/}" + LIB_CODE="${LIB_CODE^^}" + [[ ${#LIB_CODE} -lt 2 ]] && LIB_CODE="HOME" + + echo "" + echo "══════════════════════════════════════════════════════" + echo " ADMIN ACCOUNT" + echo "══════════════════════════════════════════════════════" + echo "" + echo " You will enter this password during the web installer." + echo " It is stored in config-main.env (chmod 600) for the post-setup script." + echo "" + + local KOHA_ADMIN_USER="" + prompt_text "Admin username [admin]:" "admin" KOHA_ADMIN_USER + [[ -z "$KOHA_ADMIN_USER" ]] && KOHA_ADMIN_USER="admin" + + local KOHA_ADMIN_PASS="" + if [ "$UNATTENDED" = true ]; then + KOHA_ADMIN_PASS="$(generate_password 20)" + else + read -rsp " Admin password [Enter = auto-generate]: " KOHA_ADMIN_PASS; echo + [[ -z "$KOHA_ADMIN_PASS" ]] && KOHA_ADMIN_PASS="$(generate_password 20)" + fi + + local KOHA_ADMIN_EMAIL="" + prompt_text "Admin email [blank to skip]:" "" KOHA_ADMIN_EMAIL + + # ── Item types ──────────────────────────────────────────────────────────── + echo "" + echo "══════════════════════════════════════════════════════" + echo " ITEM TYPES" + echo "══════════════════════════════════════════════════════" + echo "" + echo " Default set: Book, DVD, Blu-ray, Magazine, Comic, Board Game" + echo " (You can add/edit more later in Staff UI → Administration → Item types)" + echo "" + + # Store as "CODE:Description:loan_days" triplets + local -a ITEM_TYPES=( + "BK:Book:21" + "DVD:DVD:7" + "BLU:Blu-ray:7" + "MAG:Magazine:14" + "COM:Comic:14" + "BG:Board Game:14" + ) + + local _add_items="" + prompt_yn "Add custom item types now? (y/N):" "n" _add_items + if [[ "$_add_items" =~ ^[Yy]$ ]]; then + echo "" + echo " Enter each type (blank code to finish)." + echo " Example: CD : Compact Disc : 14" + echo "" + local _ic _id _il + while true; do + prompt_text " Code (3-5 chars, blank to finish):" "" _ic + [[ -z "$_ic" ]] && break + _ic="${_ic//[^A-Za-z0-9]/}" + _ic="${_ic^^}" + prompt_text " Description for '$_ic':" "$_ic" _id + prompt_text " Loan period in days [14]:" "14" _il + _il="${_il//[^0-9]/}"; [[ -z "$_il" ]] && _il="14" + ITEM_TYPES+=("${_ic}:${_id}:${_il}") + log_success " Added: $_ic — $_id (${_il}d)" + done + fi + + # ── Shelf locations ─────────────────────────────────────────────────────── + echo "" + echo "══════════════════════════════════════════════════════" + echo " SHELF LOCATIONS" + echo "══════════════════════════════════════════════════════" + echo "" + echo " Define where books live (e.g. LR1=Living Room Shelf 1, BR=Bedroom)." + echo " Used on item records so you can find them again." + echo " (You can add/edit more later in Staff UI → Administration → Authorized values → LOC)" + echo "" + + local -a SHELF_LOCS=() + local _add_locs="" + prompt_yn "Add shelf locations now? (y/N):" "n" _add_locs + if [[ "$_add_locs" =~ ^[Yy]$ ]]; then + echo "" + local _lc _ld + while true; do + prompt_text " Location code (blank to finish):" "" _lc + [[ -z "$_lc" ]] && break + _lc="${_lc//[^A-Za-z0-9_]/_}" + prompt_text " Description for '$_lc':" "$_lc" _ld + SHELF_LOCS+=("${_lc}:${_ld}") + log_success " Added: $_lc — $_ld" + done + fi + + # ── Passwords ───────────────────────────────────────────────────────────── + local DB_PASS DB_ROOT_PASS RABBIT_PASS + DB_PASS="$(generate_password 24)" + DB_ROOT_PASS="$(generate_password 24)" + RABBIT_PASS="$(generate_password 24)" + + # ── Create directories ──────────────────────────────────────────────────── + mkdir -p "$KOHA_DIR/data" ensure_docker_dir_ownership "$KOHA_DIR" cd "$KOHA_DIR" || return 1 + # ── docker-compose.yml ──────────────────────────────────────────────────── cat > docker-compose.yml << 'KOHA_COMPOSE' name: koha @@ -257,7 +383,7 @@ services: MYSQL_USER: koha_default MYSQL_PASSWORD: ${DB_PASS} volumes: - - koha_db_data:/var/lib/mysql + - ./data:/var/lib/mysql networks: - koha_internal @@ -280,9 +406,6 @@ services: networks: - koha_internal -volumes: - koha_db_data: - networks: koha_internal: internal: true @@ -291,32 +414,208 @@ networks: name: ${CADDY_NET:-caddy_net} KOHA_COMPOSE + # ── config-main.env ─────────────────────────────────────────────────────── cat > config-main.env << KOHA_ENV # Koha ILS configuration — generated at install time MYSQL_SERVER=koha-db DB_NAME=koha_default MYSQL_USER=koha_default MYSQL_PASSWORD=$DB_PASS -DB_ROOT_PASS=$(generate_password 24) +DB_ROOT_PASS=$DB_ROOT_PASS +DB_PASS=$DB_PASS MEMCACHED_SERVERS=koha-memcached:11211 MB_HOST=koha-rabbitmq MB_PORT=61613 MB_USER=koha MB_PASS=$RABBIT_PASS RABBIT_PASS=$RABBIT_PASS -DB_PASS=$DB_PASS CADDY_NET=$SITE_CADDY_NET KOHA_LANGS=en ZEBRA_MARC_FORMAT=marc21 USE_Z3950=1 +# Admin credentials (used by post-setup.sh after web installer) +KOHA_ADMIN_USER=$KOHA_ADMIN_USER +KOHA_ADMIN_PASS=$KOHA_ADMIN_PASS KOHA_ENV chmod 600 config-main.env ensure_docker_dir_ownership "$KOHA_DIR" + + # ── Generate post-setup.sh ──────────────────────────────────────────────── + log_info "Writing post-setup.sh ..." + + # Serialise item types and shelf locs into env-safe strings + local _items_str _locs_str + _items_str="$(IFS='|'; echo "${ITEM_TYPES[*]}")" + _locs_str="$(IFS='|'; echo "${SHELF_LOCS[*]}")" + + cat > post-setup.sh << POSTSETUP +#!/bin/bash +# post-setup.sh — Run AFTER completing the Koha web installer at http://localhost:8098 +# Configures library, item types, and shelf locations via the Koha REST API. +# Generated by ubuntu-post-install on $(date '+%F'). + +set -uo pipefail + +KOHA_STAFF_URL="http://localhost:8098" +KOHA_DIR="${KOHA_DIR}" +CONF="\${KOHA_DIR}/config-main.env" + +[ -f "\$CONF" ] || { echo "config-main.env not found at \$CONF"; exit 1; } +source "\$CONF" + +ADMIN_USER="\${KOHA_ADMIN_USER:-admin}" +ADMIN_PASS="\${KOHA_ADMIN_PASS:-}" +LIB_NAME="${LIB_NAME}" +LIB_CODE="${LIB_CODE}" +ITEMS_RAW="${_items_str}" +LOCS_RAW="${_locs_str}" + +log() { echo "[\$(date '+%T')] \$*"; } +ok() { echo " ✓ \$*"; } +warn() { echo " ⚠ \$*"; } +fail() { echo " ✗ \$*" >&2; } + +echo "" +echo "╔═══════════════════════════════════════════════════════╗" +echo "║ Koha Post-Setup Configuration ║" +echo "╚═══════════════════════════════════════════════════════╝" +echo "" +echo " Library : \$LIB_NAME (\$LIB_CODE)" +echo " Admin : \$ADMIN_USER" +echo "" + +# ── Wait for Koha API ──────────────────────────────────────────────────────── +log "Waiting for Koha REST API..." +_tries=0 +until curl -sf "\$KOHA_STAFF_URL/api/v1/auth/session" -o /dev/null 2>/dev/null; do + _tries=\$((_tries+1)) + [ "\$_tries" -gt 60 ] && { fail "Koha API not available after 5 min — is the stack running?"; exit 1; } + printf "." + sleep 5 +done +echo "" +ok "Koha API is up" + +# ── Authenticate ───────────────────────────────────────────────────────────── +if [ -z "\$ADMIN_PASS" ]; then + read -rsp " Admin password (from web installer): " ADMIN_PASS; echo +fi + +_COOKIE="\$(mktemp)" +trap 'rm -f "\$_COOKIE"' EXIT + +_auth_resp=\$(curl -sf -c "\$_COOKIE" -X POST "\$KOHA_STAFF_URL/api/v1/auth/session" \\ + -H "Content-Type: application/json" \\ + -d "{\"userid\":\"\$ADMIN_USER\",\"password\":\"\$ADMIN_PASS\"}" 2>&1) || true +if ! curl -sf -b "\$_COOKIE" "\$KOHA_STAFF_URL/api/v1/libraries" -o /dev/null 2>/dev/null; then + fail "Authentication failed — check admin username/password in config-main.env" + echo " You can update KOHA_ADMIN_PASS in \$CONF and re-run this script." + exit 1 +fi +ok "Authenticated as \$ADMIN_USER" + +# ── Helper: POST with JSON ──────────────────────────────────────────────────── +koha_post() { + local _endpoint="\$1" _body="\$2" + curl -sf -b "\$_COOKIE" -X POST "\$KOHA_STAFF_URL/api/v1/\$_endpoint" \\ + -H "Content-Type: application/json" \\ + -d "\$_body" -o /dev/null -w "%{http_code}" +} + +koha_patch() { + local _endpoint="\$1" _body="\$2" + curl -sf -b "\$_COOKIE" -X PATCH "\$KOHA_STAFF_URL/api/v1/\$_endpoint" \\ + -H "Content-Type: application/json" \\ + -d "\$_body" -o /dev/null -w "%{http_code}" +} + +# ── Library branch ──────────────────────────────────────────────────────────── +echo "" +log "Creating library branch '\$LIB_CODE' (\$LIB_NAME)..." +_code=\$(koha_post "libraries" "{\"library_id\":\"\$LIB_CODE\",\"name\":\"\$LIB_NAME\"}") +case "\$_code" in + 201) ok "Library created" ;; + 409) warn "Library '\$LIB_CODE' already exists — skipping" ;; + *) warn "Unexpected response \$_code — may need manual setup in Staff UI" ;; +esac + +# ── System preferences ──────────────────────────────────────────────────────── +log "Setting system preferences..." +docker exec koha bash -c "\\ + mysql -u root -p\${DB_ROOT_PASS:-\$DB_ROOT_PASS} koha_default -e \\ + \\\"UPDATE systempreferences SET value='\$LIB_NAME' WHERE variable='LibraryName';\\\" +" 2>/dev/null && ok "LibraryName → \$LIB_NAME" || warn "Could not set LibraryName (set manually in Staff UI → Admin → System preferences → OPAC)" + +docker exec koha bash -c "\\ + mysql -u root -p\${DB_ROOT_PASS:-\$DB_ROOT_PASS} koha_default -e \\ + \\\"UPDATE systempreferences SET value='\$LIB_NAME' WHERE variable='OPACLibraryName';\\\" +" 2>/dev/null && ok "OPACLibraryName → \$LIB_NAME" || true + +# ── Item types ──────────────────────────────────────────────────────────────── +echo "" +log "Creating item types..." +IFS='|' read -ra _ITEMS <<< "\$ITEMS_RAW" +for _item in "\${_ITEMS[@]}"; do + IFS=':' read -r _ic _id _il <<< "\$_item" + [[ -z "\$_ic" ]] && continue + _body="{\"item_type_id\":\"\$_ic\",\"description\":\"\$_id\",\"loan_period\":\$_il,\"renewals_allowed\":99}" + _code=\$(koha_post "item_types" "\$_body") + case "\$_code" in + 201) ok "\$_ic — \$_id (\${_il}d loan)" ;; + 409) warn "\$_ic already exists — skipping" ;; + *) warn "\$_ic: unexpected response \$_code" ;; + esac +done + +# ── Shelf locations (LOC authorized values) ─────────────────────────────────── +if [ -n "\$LOCS_RAW" ]; then + echo "" + log "Creating shelf locations..." + IFS='|' read -ra _LOCS <<< "\$LOCS_RAW" + for _loc in "\${_LOCS[@]}"; do + IFS=':' read -r _lc _ld <<< "\$_loc" + [[ -z "\$_lc" ]] && continue + _body="{\"authorised_value\":\"\$_lc\",\"lib\":\"\$_ld\",\"lib_opac\":\"\$_ld\"}" + _code=\$(koha_post "authorised_value_categories/LOC/authorised_values" "\$_body") + case "\$_code" in + 201) ok "\$_lc — \$_ld" ;; + 409) warn "\$_lc already exists — skipping" ;; + *) warn "\$_lc: unexpected response \$_code" ;; + esac + done +fi + +echo "" +echo "══════════════════════════════════════════════════════" +echo " SETUP COMPLETE" +echo "══════════════════════════════════════════════════════" +echo "" +echo " Your Koha library is ready to use." +echo "" +echo " OPAC (patron browsing): http://localhost:8097" +echo " Staff / admin: http://localhost:8098" +echo "" +echo " Next steps in Staff UI:" +echo " 1. Administration → Patron categories → add patron types" +echo " (e.g. ADULT, CHILD, FAMILY)" +echo " 2. Administration → Circulation and fines rules → set loan rules" +echo " 3. Cataloguing → Z39.50/SRU — verify WorldCat/OpenLibrary targets work" +echo " (test by searching an ISBN)" +echo "" +echo " Adding books:" +echo " Cataloguing → Z39.50/SRU search → enter ISBN" +echo " Or use a USB barcode scanner — scan the ISBN barcode on any book" +echo "" +POSTSETUP + + chmod +x post-setup.sh + chown "$ACTUAL_USER:$ACTUAL_USER" post-setup.sh 2>/dev/null || true + log_success "post-setup.sh written" + log_success "Koha configured at $KOHA_DIR" # Koha has its own staff login — no Authelia for staff interface - # OPAC is public-facing, Caddy proxies to OPAC port configure_caddy_for_service "Koha OPAC" "koha:8080" "library" write_readme "$KOHA_DIR" << MD @@ -328,45 +627,64 @@ Full Integrated Library System for managing a physical book collection. - **OPAC** (patron browsing): http://localhost:8097 - **Staff / admin**: http://localhost:8098 -## First-time setup (important — takes ~5 minutes) -1. Open the **staff interface**: http://localhost:8098 -2. Wait for the setup wizard (Koha takes 2–3 minutes to initialize on first start) -3. Follow the web installer — it asks for library name, MARC flavour (choose MARC21), - and creates your admin account -4. Go to Administration → Basic parameters → Libraries to add your library -5. Go to Administration → Basic parameters → Item types to define your book categories -6. Go to Administration → Basic parameters → Authorized values → LOC to define - shelf locations (e.g. "LR1" = Living Room Shelf 1) +## Quick setup (4 steps) + +### Step 1 — Start the stack +\`\`\`bash +cd $KOHA_DIR +docker compose up -d +\`\`\` +Takes 2–3 minutes on first boot while Koha initialises. + +### Step 2 — Complete the web installer +1. Open **http://localhost:8098** +2. You may see a "Database connection" page first — wait 1–2 min and refresh +3. The installer wizard appears automatically: + - **Language**: click "Install for language English" → Continue + - **Koha database**: fields are pre-filled from env → Continue + - **Select MARC flavour**: choose **MARC21** → Continue + - **Install basic Koha data**: check all boxes → Continue + - **Set Koha administrator password**: enter **$KOHA_ADMIN_PASS** exactly + - **Finish**: click the login link +4. Log in with username **$KOHA_ADMIN_USER** and the password above + +### Step 3 — Run post-setup +\`\`\`bash +sudo $KOHA_DIR/post-setup.sh +\`\`\` +Auto-creates your library branch, item types, and shelf locations via the REST API. + +### Step 4 — Add patron categories and circulation rules +In Staff UI: +- Administration → Patron categories → New category (e.g. ADULT, CHILD) +- Administration → Circulation and fines rules → add a rule for your library ## Adding books -- **By ISBN** (recommended): Cataloguing → Z39.50/SRU search → enter ISBN → - imports full metadata, cover art, summary from WorldCat/OpenLibrary -- **Barcode scanning**: use any USB barcode scanner or phone camera app; - scan ISBN on the back of the book +- **By ISBN** (recommended): Cataloguing → Z39.50/SRU search → enter ISBN + → imports metadata, cover art, and summary from WorldCat / Open Library +- **Barcode scanner**: any USB scanner works; scan the ISBN barcode on the book cover ## Loans / checkout -- Patron management: Patrons → New patron (add family members) -- Checkout: Circulation → Check out → scan patron card, scan book barcode +- Add patrons: Patrons → New patron +- Checkout: Circulation → Check out → scan/enter patron card, scan book barcode - Return: Circulation → Check in +## Data location +- **Database**: \`$KOHA_DIR/data/\` (MariaDB bind-mount, included in backup) +- **Config**: \`$KOHA_DIR/config-main.env\` (chmod 600) + ## Manage \`\`\`bash cd $KOHA_DIR -docker compose up -d # start (allow 3 min for first-time init) -docker compose down # stop -docker compose logs -f # logs +docker compose up -d # start (3 min first boot) +docker compose down # stop +docker compose logs -f # logs +docker compose logs -f koha # Koha app logs only docker compose pull && docker compose up -d # update \`\`\` - -## Shelf locations -Define custom locations in Staff → Administration → Authorized values → LOST -(or create a new category). Common home library codes: -- LR1, LR2 — Living Room shelves -- BR — Bedroom -- OF — Office -- BS — Basement MD + # ── Start ────────────────────────────────────────────────────────────────── local START_KOHA="" prompt_yn "Start Koha now? (y/n):" "y" START_KOHA if [ "$START_KOHA" = "y" ] || [ "$START_KOHA" = "Y" ]; then @@ -376,10 +694,27 @@ MD fi echo "" - echo " OPAC (patron UI): http://localhost:8097" - echo " Staff / admin: http://localhost:8098" - echo " First run: open staff interface and complete the setup wizard (~5 min)" - echo " Credentials saved in: $KOHA_DIR/config-main.env" + echo "╔═══════════════════════════════════════════════════════════════════╗" + echo "║ KOHA SETUP SUMMARY ║" + echo "╚═══════════════════════════════════════════════════════════════════╝" + echo "" + echo " Library name : $LIB_NAME ($LIB_CODE)" + echo " Admin user : $KOHA_ADMIN_USER" + echo " Admin pass : $KOHA_ADMIN_PASS" + echo "" + echo " ┌──────────────────────────────────────────────────────────────┐" + echo " │ IMPORTANT — Write down or save the admin password above. │" + echo " │ You will type it during the web installer in Step 3. │" + echo " │ Also stored in: $KOHA_DIR/config-main.env │" + echo " └──────────────────────────────────────────────────────────────┘" + echo "" + echo " NEXT STEPS:" + echo " 1. Wait ~3 min, then open: http://localhost:8098" + echo " 2. Complete the web installer (use password above when asked)" + echo " 3. Run: sudo $KOHA_DIR/post-setup.sh" + echo "" + echo " OPAC (patron UI): http://localhost:8097" + echo " Staff / admin: http://localhost:8098" echo "" } From 5b340552c3919ffa932506575bd414c1c6a4ebbd Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 10 Jun 2026 00:49:41 +0000 Subject: [PATCH 2/2] Replace CADDY_REMOTE_HOST with explicit CADDY_MODE in site config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old CADDY_REMOTE_HOST variable was confusingly named — it sounded like the Caddy server's address but actually stored this machine's IP (so Caddy knew how to reach services here). Services don't need to know where Caddy is; they only need to know whether to write a Caddyfile or create a snippet. Changes in lib/common.sh: - Add CADDY_MODE=local|remote|none as the authoritative setting - load_site_config: parse CADDY_MODE; if old CADDY_REMOTE_HOST present and CADDY_MODE unset, infer CADDY_MODE=remote (backward compat) - save_site_config: write CADDY_MODE, drop CADDY_REMOTE_HOST output - configure_caddy_for_service: use CADDY_MODE for mode detection; for remote snippets auto-detect this machine's primary IP via hostname -I instead of requiring a stored value (still falls back to CADDY_REMOTE_HOST if present in an old .config) Changes in setup.sh (run_site_configure wizard): - Replace free-text "Caddy remote host" prompt with a 3-choice menu: [1] This machine [2] Remote machine [3] None/skip - Existing installs with CADDY_REMOTE_HOST pre-select option 2 automatically https://claude.ai/code/session_01S7UecmQRG6CKTYPoBqbVLj --- lib/common.sh | 49 +++++++++++++++++++++++++++++-------------------- setup.sh | 27 +++++++++++++++++++++------ 2 files changed, 50 insertions(+), 26 deletions(-) diff --git a/lib/common.sh b/lib/common.sh index cbf08f8..340828d 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -56,7 +56,8 @@ SITE_DOMAIN="" SITE_CADDY_NET="caddy_net" SITE_PUID="" SITE_PGID="" -CADDY_REMOTE_HOST="" # LAN IP/hostname of this machine, used when Caddy runs elsewhere +CADDY_MODE="" # local | remote | none (set by site configure wizard) +CADDY_REMOTE_HOST="" # legacy — kept for backward compat with old .config files load_site_config() { local cfg="$DOCKER_DIR/.config" @@ -66,16 +67,19 @@ load_site_config() { [[ "$key" =~ ^[[:space:]]*# ]] && continue [[ -z "${key// }" ]] && continue case "$key" in - SITE_TZ) SITE_TZ="$val" ;; - SITE_DOMAIN) SITE_DOMAIN="$val" ;; + SITE_TZ) SITE_TZ="$val" ;; + SITE_DOMAIN) SITE_DOMAIN="$val" ;; SITE_CADDY_NET) SITE_CADDY_NET="$val" ;; SITE_PUID) SITE_PUID="$val" ;; SITE_PGID) SITE_PGID="$val" ;; + CADDY_MODE) CADDY_MODE="$val" ;; CADDY_REMOTE_HOST) CADDY_REMOTE_HOST="$val" ;; BASE_DOMAIN) [ -z "$SITE_DOMAIN" ] && SITE_DOMAIN="$val" ;; esac done < "$cfg" - export SITE_TZ SITE_DOMAIN SITE_CADDY_NET SITE_PUID SITE_PGID CADDY_REMOTE_HOST + # Backward compat: old installs used CADDY_REMOTE_HOST to signal remote mode + [ -z "$CADDY_MODE" ] && [ -n "$CADDY_REMOTE_HOST" ] && CADDY_MODE="remote" + export SITE_TZ SITE_DOMAIN SITE_CADDY_NET SITE_PUID SITE_PGID CADDY_MODE CADDY_REMOTE_HOST } save_site_config() { @@ -87,11 +91,11 @@ save_site_config() { [ -n "$SITE_TZ" ] && echo "SITE_TZ=$SITE_TZ" [ -n "$SITE_DOMAIN" ] && echo "SITE_DOMAIN=$SITE_DOMAIN" [ -n "$SITE_CADDY_NET" ] && echo "SITE_CADDY_NET=$SITE_CADDY_NET" - [ -n "$SITE_PUID" ] && echo "SITE_PUID=$SITE_PUID" - [ -n "$SITE_PGID" ] && echo "SITE_PGID=$SITE_PGID" - [ -n "$CADDY_REMOTE_HOST" ] && echo "CADDY_REMOTE_HOST=$CADDY_REMOTE_HOST" + [ -n "$SITE_PUID" ] && echo "SITE_PUID=$SITE_PUID" + [ -n "$SITE_PGID" ] && echo "SITE_PGID=$SITE_PGID" + [ -n "$CADDY_MODE" ] && echo "CADDY_MODE=$CADDY_MODE" # Backward-compat alias for services that still read BASE_DOMAIN directly - [ -n "$SITE_DOMAIN" ] && echo "BASE_DOMAIN=$SITE_DOMAIN" + [ -n "$SITE_DOMAIN" ] && echo "BASE_DOMAIN=$SITE_DOMAIN" } > "$cfg" chmod 600 "$cfg" } @@ -280,12 +284,11 @@ configure_caddy_for_service() { esac # ── Determine Caddy mode ────────────────────────────────────────────────── - # local: Caddy container running on this machine → write Caddyfile + reload - # remote: Caddy on another machine → generate snippet file to copy over - # none: no Caddy anywhere → silent return - local _CADDY_MODE="none" - [ -d "$DOCKER_DIR/caddy" ] && _CADDY_MODE="local" - [ -n "$CADDY_REMOTE_HOST" ] && [ "$_CADDY_MODE" != "local" ] && _CADDY_MODE="remote" + # Explicit CADDY_MODE (set by site wizard) takes priority. + # Fall back to: local if ~/docker/caddy exists, remote if legacy CADDY_REMOTE_HOST set. + local _CADDY_MODE="${CADDY_MODE:-none}" + [ "$_CADDY_MODE" = "none" ] && [ -d "$DOCKER_DIR/caddy" ] && _CADDY_MODE="local" + [ "$_CADDY_MODE" = "none" ] && [ -n "${CADDY_REMOTE_HOST:-}" ] && _CADDY_MODE="remote" [ "$_CADDY_MODE" = "none" ] && return 0 echo "" @@ -294,11 +297,10 @@ configure_caddy_for_service() { echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" if [ "$_CADDY_MODE" = "remote" ]; then - echo " Remote Caddy configured ($CADDY_REMOTE_HOST)." - echo " A snippet file will be saved to ~/docker/caddy-snippets/ for you to" - echo " copy to your Caddy machine." + echo " Caddy is on a remote machine — a snippet file will be saved to" + echo " ~/docker/caddy-snippets/ for you to copy to your Caddy machine." else - echo "Caddy is installed. You can configure a reverse proxy for $SERVICE_NAME." + echo " Caddy is installed on this machine." fi echo "" @@ -329,8 +331,15 @@ configure_caddy_for_service() { # Build the site block — upstream differs by mode local _BLOCK_UPSTREAM="$_UPSTREAM" if [ "$_CADDY_MODE" = "remote" ]; then - # Remote Caddy can't resolve Docker container names — use host IP + published port - _BLOCK_UPSTREAM="${CADDY_REMOTE_HOST}:${_DISPLAY_PORT}" + # Remote Caddy can't resolve Docker container names — use this machine's IP + published port. + # Prefer legacy CADDY_REMOTE_HOST if set (old installs that stored it explicitly), + # otherwise auto-detect the primary non-loopback IP. + local _THIS_IP="${CADDY_REMOTE_HOST:-}" + if [ -z "$_THIS_IP" ]; then + _THIS_IP="$(hostname -I 2>/dev/null | awk '{print $1}')" + fi + [ -z "$_THIS_IP" ] && _THIS_IP="$(hostname -f 2>/dev/null || echo "127.0.0.1")" + _BLOCK_UPSTREAM="${_THIS_IP}:${_DISPLAY_PORT}" fi local _SITE_BLOCK diff --git a/setup.sh b/setup.sh index 2fa7162..a534991 100755 --- a/setup.sh +++ b/setup.sh @@ -122,19 +122,34 @@ run_site_configure() { local _cur_tz="${SITE_TZ:-$_sys_tz}" local _cur_dom="${SITE_DOMAIN:-}" local _cur_net="${SITE_CADDY_NET:-caddy_net}" - local _cur_caddy_host="${CADDY_REMOTE_HOST:-}" + # Resolve current Caddy mode for display — handle legacy CADDY_REMOTE_HOST + local _cur_mode="${CADDY_MODE:-}" + [ -z "$_cur_mode" ] && [ -n "${CADDY_REMOTE_HOST:-}" ] && _cur_mode="remote" + [ -z "$_cur_mode" ] && _cur_mode="local" prompt_text " Timezone [${_cur_tz}]:" "$_cur_tz" SITE_TZ prompt_text " Base domain (e.g., example.com) [${_cur_dom:-}]:" "$_cur_dom" SITE_DOMAIN prompt_text " Caddy Docker network [${_cur_net}]:" "$_cur_net" SITE_CADDY_NET echo "" - echo " Caddy location: leave blank if Caddy runs on THIS machine (default)." - echo " Set to this machine's LAN IP or hostname if Caddy runs on a DIFFERENT" - echo " machine — service installers will generate snippet files to copy over." - prompt_text " Caddy remote host (LAN IP/hostname) [${_cur_caddy_host:-}]:" "$_cur_caddy_host" CADDY_REMOTE_HOST + echo " Where does Caddy run?" + echo " [1] This machine — Caddy installed here (default)" + echo " [2] Remote machine — different server, VPN node, or Netbird peer" + echo " (service installers save snippet files to ~/docker/caddy-snippets/)" + echo " [3] None / skip — configure Caddy later" + echo "" + local _caddy_default="1" + case "$_cur_mode" in remote) _caddy_default="2" ;; none) _caddy_default="3" ;; esac + local _caddy_choice="" + prompt_text " Caddy location [${_caddy_default}]:" "$_caddy_default" _caddy_choice + case "${_caddy_choice:-$_caddy_default}" in + 2) CADDY_MODE="remote" ;; + 3) CADDY_MODE="none" ;; + *) CADDY_MODE="local" ;; + esac + CADDY_REMOTE_HOST="" # clear legacy value; CADDY_MODE is authoritative now - export SITE_TZ SITE_DOMAIN SITE_CADDY_NET CADDY_REMOTE_HOST + export SITE_TZ SITE_DOMAIN SITE_CADDY_NET CADDY_MODE CADDY_REMOTE_HOST mkdir -p "$DOCKER_DIR" save_site_config log_success "Saved to $DOCKER_DIR/.config"