From 4add1684c3a3b23a9bb01ce2fd7ddabda081b23a Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 14 Jan 2026 12:43:20 -0800 Subject: [PATCH 1/4] Enable additional ZIM archives for kiwix New archives (~95G total): - Project Gutenberg 2023 (72G) - 60,000+ public domain books - iFixit (3.3G) - Repair guides - Stack Exchange: SuperUser (3.7G), Math (6.9G) - LibreTexts: Biology, Chemistry, Engineering, Mathematics, Physics, Humanities Also: - Fix transmission to only restart when config changes - Update CLAUDE.md to use full ansible paths Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 8 ++-- ansible/roles/kiwix/defaults/main.yml | 50 ++++++++++++----------- ansible/roles/transmission/tasks/main.yml | 11 +++++ 3 files changed, 40 insertions(+), 29 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index dcc4054..eece6d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -81,15 +81,13 @@ After creating a PR, run `open ` to open it in the browser (Claude Code' ## Ansible -Run playbooks from the `ansible/` directory. - ```bash # Install collection dependencies -ansible-galaxy collection install -r requirements.yml +ansible-galaxy collection install -r ansible/requirements.yml # Dry-run before committing changes -ansible-playbook playbooks/indri.yml --check --diff +ansible-playbook ansible/playbooks/indri.yml --check --diff # Apply changes -ansible-playbook playbooks/indri.yml +ansible-playbook ansible/playbooks/indri.yml ``` diff --git a/ansible/roles/kiwix/defaults/main.yml b/ansible/roles/kiwix/defaults/main.yml index c88fd16..21f5932 100644 --- a/ansible/roles/kiwix/defaults/main.yml +++ b/ansible/roles/kiwix/defaults/main.yml @@ -23,40 +23,42 @@ kiwix_zim_archives: # - category: wikipedia # filename: wikipedia_en_top_maxi_2025-12.zim # 7.6G - Top 100K articles - ## Project Gutenberg - Public domain books - # - category: gutenberg - # filename: gutenberg_en_all_2023-08.zim # 72G - Full collection (2023) + # Project Gutenberg - Public domain books (72G) + - category: gutenberg + filename: gutenberg_en_all_2023-08.zim + + ## Newer Gutenberg (much larger, unclear why): # - category: gutenberg # filename: gutenberg_en_all_2025-11.zim # 206G - Full collection (2025) - ## iFixit - Repair guides - # - category: ifixit - # filename: ifixit_en_all_2025-12.zim # 3.3G + # iFixit - Repair guides (3.3G) + - category: ifixit + filename: ifixit_en_all_2025-12.zim - ## Stack Exchange - # - category: stack_exchange - # filename: superuser.com_en_all_2025-12.zim # 3.7G + # Stack Exchange + - category: stack_exchange + filename: superuser.com_en_all_2025-12.zim # 3.7G # - category: stack_exchange # filename: serverfault.com_en_all_2025-12.zim # 1.5G # - category: stack_exchange # filename: askubuntu.com_en_all_2025-12.zim # 2.6G # - category: stack_exchange # filename: unix.stackexchange.com_en_all_2025-12.zim # 1.2G - # - category: stack_exchange - # filename: math.stackexchange.com_en_all_2025-12.zim # 6.9G + - category: stack_exchange + filename: math.stackexchange.com_en_all_2025-12.zim # 6.9G # - category: stack_exchange # filename: stackoverflow.com_en_all_2023-11.zim # 75G - Full StackOverflow - ## LibreTexts - Open educational resources - # - category: libretexts - # filename: libretexts_en_biology_2025-01.zim # 2.1G - # - category: libretexts - # filename: libretexts_en_chemistry_2025-01.zim # 2.0G - # - category: libretexts - # filename: libretexts_en_engineering_2025-01.zim # 647M - # - category: libretexts - # filename: libretexts_en_mathematics_2025-01.zim # 744M - # - category: libretexts - # filename: libretexts_en_physics_2025-01.zim # 464M - # - category: libretexts - # filename: libretexts_en_humanities_2025-01.zim # 3.5G + # LibreTexts - Open educational resources + - category: libretexts + filename: libretexts.org_en_bio_2025-01.zim # 2.1G + - category: libretexts + filename: libretexts.org_en_chem_2025-01.zim # 2.0G + - category: libretexts + filename: libretexts.org_en_eng_2025-01.zim # 647M + - category: libretexts + filename: libretexts.org_en_math_2025-01.zim # 744M + - category: libretexts + filename: libretexts.org_en_phys_2025-01.zim # 464M + - category: libretexts + filename: libretexts.org_en_human_2025-01.zim # 3.5G diff --git a/ansible/roles/transmission/tasks/main.yml b/ansible/roles/transmission/tasks/main.yml index d2e3fda..463ecd8 100644 --- a/ansible/roles/transmission/tasks/main.yml +++ b/ansible/roles/transmission/tasks/main.yml @@ -21,8 +21,19 @@ path: ~/.config/transmission-daemon state: absent +# Note: transmission must be stopped before modifying settings.json +# otherwise it may overwrite our changes on shutdown +- name: Check if settings.json needs updating + ansible.builtin.template: + src: settings.json.j2 + dest: "{{ transmission_config_dir }}/settings.json" + mode: '0644' + check_mode: true + register: settings_check + - name: Stop transmission before config changes ansible.builtin.command: brew services stop transmission-cli + when: settings_check.changed register: brew_stop changed_when: false failed_when: false -- 2.50.1 (Apple Git-155) From 3847c12b42d50920afd33cabcc030a05c863643b Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 14 Jan 2026 13:03:46 -0800 Subject: [PATCH 2/4] Fix transmission config to prevent perpetual ansible diffs - Expand settings.json template to include all transmission defaults - Use static pre-hashed rpc-password so transmission doesn't regenerate - Change file mode from 0644 to 0600 to match transmission's default - Add Jinja comment explaining the RPC password workaround Co-Authored-By: Claude Opus 4.5 --- ansible/roles/transmission/tasks/main.yml | 4 +- .../transmission/templates/settings.json.j2 | 82 +++++++++++++++++-- 2 files changed, 77 insertions(+), 9 deletions(-) diff --git a/ansible/roles/transmission/tasks/main.yml b/ansible/roles/transmission/tasks/main.yml index 463ecd8..9b941ca 100644 --- a/ansible/roles/transmission/tasks/main.yml +++ b/ansible/roles/transmission/tasks/main.yml @@ -27,7 +27,7 @@ ansible.builtin.template: src: settings.json.j2 dest: "{{ transmission_config_dir }}/settings.json" - mode: '0644' + mode: '0600' check_mode: true register: settings_check @@ -42,7 +42,7 @@ ansible.builtin.template: src: settings.json.j2 dest: "{{ transmission_config_dir }}/settings.json" - mode: '0644' + mode: '0600' notify: restart transmission - name: Ensure transmission service is started diff --git a/ansible/roles/transmission/templates/settings.json.j2 b/ansible/roles/transmission/templates/settings.json.j2 index 6875415..36b0513 100644 --- a/ansible/roles/transmission/templates/settings.json.j2 +++ b/ansible/roles/transmission/templates/settings.json.j2 @@ -1,21 +1,89 @@ +{# + RPC is required for transmission-remote CLI to manage torrents. + Config is secure: bound to localhost only, no auth needed. + + rpc-password uses a static hash starting with '{' so transmission + recognizes it as pre-hashed and won't regenerate it on restart. + Without this, transmission writes a new hash each startup causing + perpetual ansible diffs. +#} { "_comment": "{{ ansible_managed }}", + "alt-speed-down": 50, + "alt-speed-enabled": false, + "alt-speed-time-begin": 540, + "alt-speed-time-day": 127, + "alt-speed-time-enabled": false, + "alt-speed-time-end": 1020, + "alt-speed-up": 50, + "announce-ip": "", + "announce-ip-enabled": false, + "anti-brute-force-enabled": false, + "anti-brute-force-threshold": 100, + "bind-address-ipv4": "0.0.0.0", + "bind-address-ipv6": "::", + "blocklist-enabled": false, + "blocklist-url": "http://www.example.com/blocklist", + "cache-size-mb": 4, + "default-trackers": "", + "dht-enabled": {{ transmission_dht_enabled | lower }}, "download-dir": "{{ transmission_download_dir }}", + "download-queue-enabled": true, + "download-queue-size": 5, + "encryption": {{ transmission_encryption }}, + "idle-seeding-limit": 30, + "idle-seeding-limit-enabled": false, "incomplete-dir": "{{ transmission_incomplete_dir }}", "incomplete-dir-enabled": true, - "dht-enabled": {{ transmission_dht_enabled | lower }}, + "lpd-enabled": true, + "message-level": 4, + "peer-congestion-algorithm": "", + "peer-limit-global": 200, + "peer-limit-per-torrent": 50, + "peer-port": 51413, + "peer-port-random-high": 65535, + "peer-port-random-low": 49152, + "peer-port-random-on-start": false, + "peer-socket-tos": "le", "pex-enabled": {{ transmission_pex_enabled | lower }}, - "encryption": {{ transmission_encryption }}, - "rpc-enabled": {{ transmission_rpc_enabled | lower }}, - "rpc-port": {{ transmission_rpc_port }}, - "rpc-bind-address": "{{ transmission_rpc_bind_address }}", + "port-forwarding-enabled": true, + "preallocation": 1, + "prefetch-enabled": true, + "queue-stalled-enabled": true, + "queue-stalled-minutes": 30, + "ratio-limit": 2, + "ratio-limit-enabled": false, + "rename-partial-files": false, "rpc-authentication-required": {{ transmission_rpc_authentication_required | lower }}, - "rpc-whitelist-enabled": {{ transmission_rpc_whitelist_enabled | lower }}, + "rpc-bind-address": "{{ transmission_rpc_bind_address }}", + "rpc-enabled": {{ transmission_rpc_enabled | lower }}, + "rpc-host-whitelist": "", + "rpc-host-whitelist-enabled": true, + "rpc-password": "{00000000000000000000000000000000000000000000000e", + "rpc-port": {{ transmission_rpc_port }}, + "rpc-socket-mode": "0750", + "rpc-url": "/transmission/", + "rpc-username": "", "rpc-whitelist": "{{ transmission_rpc_whitelist }}", + "rpc-whitelist-enabled": {{ transmission_rpc_whitelist_enabled | lower }}, + "scrape-paused-torrents-enabled": true, + "script-torrent-added-enabled": false, + "script-torrent-added-filename": "", + "script-torrent-done-enabled": false, + "script-torrent-done-filename": "", + "script-torrent-done-seeding-enabled": false, + "script-torrent-done-seeding-filename": "", + "seed-queue-enabled": false, + "seed-queue-size": 10, "speed-limit-down": {{ transmission_speed_limit_down }}, "speed-limit-down-enabled": {{ (transmission_speed_limit_down > 0) | lower }}, "speed-limit-up": {{ transmission_speed_limit_up }}, "speed-limit-up-enabled": {{ (transmission_speed_limit_up > 0) | lower }}, "start-added-torrents": true, - "trash-original-torrent-files": false + "tcp-enabled": true, + "torrent-added-verify-mode": "fast", + "trash-original-torrent-files": false, + "umask": "022", + "upload-slots-per-torrent": 8, + "utp-enabled": true } -- 2.50.1 (Apple Git-155) From eb2f5b44cdb18b2f751ad060c9a162e37441e2f7 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 14 Jan 2026 13:17:55 -0800 Subject: [PATCH 3/4] Fix kiwix plist to only include available ZIM archives The LaunchAgent plist now dynamically includes only ZIM files that actually exist in the kiwix directory, rather than all configured archives. This prevents kiwix-serve from crashing when torrents are still downloading. Co-Authored-By: Claude Opus 4.5 --- ansible/roles/kiwix/tasks/main.yml | 12 ++++++++++++ ansible/roles/kiwix/templates/kiwix-serve.plist.j2 | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/ansible/roles/kiwix/tasks/main.yml b/ansible/roles/kiwix/tasks/main.yml index b1637f4..39794d3 100644 --- a/ansible/roles/kiwix/tasks/main.yml +++ b/ansible/roles/kiwix/tasks/main.yml @@ -158,6 +158,18 @@ - not item.stat.exists notify: restart kiwix-serve +# --- Determine which archives are available --- +- name: Find available ZIM archives in kiwix directory + ansible.builtin.find: + paths: "{{ kiwix_zim_dir }}" + patterns: "*.zim" + file_type: any # includes symlinks + register: available_zim_files + +- name: Build list of available archive filenames + ansible.builtin.set_fact: + kiwix_available_archives: "{{ available_zim_files.files | map(attribute='path') | map('basename') | list }}" + # --- LaunchAgent deployment --- - name: Deploy kiwix-serve LaunchAgent plist ansible.builtin.template: diff --git a/ansible/roles/kiwix/templates/kiwix-serve.plist.j2 b/ansible/roles/kiwix/templates/kiwix-serve.plist.j2 index 5d84916..26a61d1 100644 --- a/ansible/roles/kiwix/templates/kiwix-serve.plist.j2 +++ b/ansible/roles/kiwix/templates/kiwix-serve.plist.j2 @@ -11,8 +11,8 @@ {{ kiwix_serve_bin }} --port={{ kiwix_port }} -{% for archive in kiwix_zim_archives %} - {{ kiwix_zim_dir }}/{{ archive.filename }} +{% for filename in kiwix_available_archives %} + {{ kiwix_zim_dir }}/{{ filename }} {% endfor %} RunAtLoad -- 2.50.1 (Apple Git-155) From 9c0ff8bb9be696e1b971aee550649b7fbab737c3 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 14 Jan 2026 13:23:05 -0800 Subject: [PATCH 4/4] Add mise task for indri service health checks - Create mise-tasks/indri-services-check script - Checks all indri services (prometheus, grafana, kiwix, transmission, forgejo) - Verifies both local service status and HTTP endpoints - Transmission RPC checked via SSH since it's localhost-only (secure) - Update CLAUDE.md with instructions to run after service changes Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 10 +++++ mise-tasks/indri-services-check | 68 +++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100755 mise-tasks/indri-services-check diff --git a/CLAUDE.md b/CLAUDE.md index eece6d1..a726252 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -91,3 +91,13 @@ ansible-playbook ansible/playbooks/indri.yml --check --diff # Apply changes ansible-playbook ansible/playbooks/indri.yml ``` + +## Service Health Checks + +After making changes to services, run the service health check to verify everything is working: + +```bash +mise run indri-services-check +``` + +This checks that all indri services (prometheus, grafana, kiwix, transmission, forgejo) are running and responding to health checks. diff --git a/mise-tasks/indri-services-check b/mise-tasks/indri-services-check new file mode 100755 index 0000000..b553de2 --- /dev/null +++ b/mise-tasks/indri-services-check @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +#MISE description="Check that all indri services are online and responding" + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +FAILED=0 + +check_service() { + local name="$1" + local check_cmd="$2" + + printf "%-20s " "$name..." + if eval "$check_cmd" > /dev/null 2>&1; then + echo -e "${GREEN}OK${NC}" + else + echo -e "${RED}FAILED${NC}" + FAILED=1 + fi +} + +check_http() { + local name="$1" + local url="$2" + + printf "%-20s " "$name..." + if curl -sf --max-time 5 "$url" > /dev/null 2>&1; then + echo -e "${GREEN}OK${NC}" + else + echo -e "${RED}FAILED${NC}" + FAILED=1 + fi +} + +echo "Checking indri services..." +echo "==========================" +echo "" + +# Check via SSH that services are running on indri +echo "Local services (via launchctl/brew services):" +check_service "prometheus" "ssh indri 'brew services list | grep prometheus | grep started'" +check_service "grafana" "ssh indri 'brew services list | grep grafana | grep started'" +check_service "transmission" "ssh indri 'brew services list | grep transmission | grep started'" +check_service "kiwix-serve" "ssh indri 'launchctl list | grep kiwix | grep -v \"^-\"'" +check_service "forgejo" "ssh indri 'brew services list | grep forgejo | grep started'" + +echo "" +echo "HTTP endpoints (via Tailscale):" +check_http "Prometheus" "http://indri:9090/-/healthy" +check_http "Grafana" "http://indri:3000/api/health" +check_http "Kiwix" "http://indri:5501/" +check_http "Forgejo" "http://indri:3001/" +# Transmission RPC is localhost-only by design, check via SSH +check_service "Transmission RPC" "ssh indri 'curl -sf http://127.0.0.1:9091/transmission/rpc'" + +echo "" +if [ $FAILED -eq 0 ]; then + echo -e "${GREEN}All services healthy!${NC}" + exit 0 +else + echo -e "${RED}Some services failed health check${NC}" + exit 1 +fi -- 2.50.1 (Apple Git-155)