- Move borgmatic config.yaml from manual to ansible-managed template
- Add postgresql_databases backup for miniflux database
- Consolidate 1Password credential fetching to playbook pre_tasks
to reduce auth prompts during full playbook runs
- Roles now check if credentials are already defined before fetching,
so they still work when running with --tags
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Without specifying a database, psql defaults to connecting to a
database named after the current user, which doesn't exist on a
fresh install.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The OAuth client acts as tag:blumeops, so it needs to own all tags
it manages on devices. This enables Pulumi to set device tags
automatically instead of requiring manual Tailscale admin console
changes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove superuser from .pgpass since it's not needed for automated
operations. Only borgmatic (with pg_read_all_data role) needs
passwordless access for pg_dump backups.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Instead of manually applying tags to indri in Tailscale admin,
use tailscale.DeviceTags resource to manage them declaratively.
This includes all service tags (grafana, forge, kiwix, devpi, loki,
pg, feed) plus homelab and blumeops tags.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- All passwords fetched from 1Password at runtime using `op` CLI
- pg_hba.conf uses scram-sha-256 everywhere (no trust mode)
- initdb uses --pwfile for secure superuser password bootstrap
- All password-handling tasks use no_log: true
- Add borgmatic user with pg_read_all_data for backup dumps
- Remove pg-setup mise task (no longer needed)
- Miniflux fetches password directly from 1Password
Requires: `op signin` before running ansible
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add postgresql ansible role (postgresql@18 via homebrew)
- Creates miniflux database and user
- Configures pg_hba.conf for local scram-sha-256 auth
- Exposed via Tailscale at pg.tail8d86e.ts.net:5432
- Add miniflux ansible role (RSS/Atom feed reader)
- Depends on postgresql role
- Configures via /opt/homebrew/etc/miniflux.conf
- Reads DB password from ~/.miniflux-db-password
- Supports first-run admin creation via miniflux_create_admin flag
- Exposed via Tailscale at feed.tail8d86e.ts.net
- Update Pulumi ACL tags (tag:pg, tag:feed)
- Update tailscale_serve role with new service definitions
- Update Alloy log collection for both services
- Update indri.yml playbook with new roles
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
## Summary
- Manage tail8d86e.ts.net ACLs, tags, and DNS via Pulumi + Python
- State stored in Pulumi Cloud (free tier) to avoid circular dependency
- OAuth authentication via 1Password for secure credential management
- New mise tasks: `tailnet-preview`, `tailnet-up`
## Architecture
Two-layer approach:
- **Layer 1 (Pulumi)**: Tailnet-wide config (ACLs, tags, DNS)
- **Layer 2 (Ansible)**: Node-local `tailscale serve` config (unchanged)
## Test plan
- [x] Exported current ACL from Tailscale API
- [x] Imported existing ACL into Pulumi state
- [x] Verified `mise run tailnet-preview` shows no changes
- [x] Verified `mise run tailnet-up` applies successfully
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: https://forge.tail8d86e.ts.net/eblume/blumeops/pulls/15
## Summary
- Add `mise run blumeops-tasks` to fetch and display tasks from Todoist
- Uses uv run script with inline dependencies (httpx, rich)
- Fetches API credential securely via 1Password CLI
- Sorts tasks by custom priority order: p1, p2, p4, p3 (backlog last)
- Documents the task discovery workflow in CLAUDE.md
## Test plan
- [x] Verified `mise run blumeops-tasks` fetches and displays tasks correctly
- [x] Confirmed priority sorting works as expected
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: https://forge.tail8d86e.ts.net/eblume/blumeops/pulls/14
## Summary
- Use async with poll: 0 for alloy and loki restart handlers
- Fire-and-forget approach prevents ansible from hanging on graceful shutdown
## Test plan
- [x] Manually verified `brew services restart grafana-alloy` works
- [x] Run full ansible playbook and verify it completes without timeout
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: https://forge.tail8d86e.ts.net/eblume/blumeops/pulls/12
Use async with poll: 0 to fire-and-forget service restarts.
These services have graceful shutdown periods that can exceed
ansible's default command timeout.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
## Summary
- Add `mise run zk-docs` task to concatenate all blumeops-tagged zettelkasten cards
- Main project card is shown first, followed by service management logs
- Uses `bat` for output (added to Brewfile)
- Args are passed through to bat for custom formatting
- Update CLAUDE.md to use zk-docs command with plain output options
- Update README.md to note zettelkasten is private with contact email
## Test plan
- [x] `mise run zk-docs` displays all 6 blumeops cards
- [x] `mise run zk-docs -- --style=header --color=never --decorations=always` shows filenames without decoration
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: https://forge.tail8d86e.ts.net/eblume/blumeops/pulls/10
## Summary
- Add ansible role for devpi-server as a transparent PyPI caching proxy
- LaunchAgent with KeepAlive runs via `mise x -- devpi-server`
- Listens on port 3141, data stored in `~/devpi`
- Health checks added to `indri-services-check` script
## Manual Setup Required (on indri, before provisioning)
1. Add to `~/.config/mise/config.toml`:
```toml
[tools]
"pipx:devpi-server" = "latest"
"pipx:devpi-web" = "latest"
"pipx:devpi-client" = "latest"
```
2. Run `mise install`
3. Initialize: `mise x -- devpi-init --serverdir ~/devpi`
## Post-Provisioning
- Set up Tailscale service `pypi` on port 443 → 3141
- Configure client pip.conf with index-url
## Test plan
- [x] Ansible syntax check passes
- [x] Dry-run: `mise run provision-indri -- --check --diff`
- [x] Apply: `mise run provision-indri`
- [x] Health check: `mise run indri-services-check`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: https://forge.tail8d86e.ts.net/eblume/blumeops/pulls/9
## Summary
- Adds a new Grafana dashboard for Node Exporter metrics on macOS hosts
- Uses macOS-native memory metrics (node_memory_total_bytes, node_memory_active_bytes, etc.) instead of Linux-specific ones
- Includes dropdown selectors for instance, disk, and network device filtering
## Details
The standard Node Exporter dashboards show "No Data" for memory panels on macOS because they query Linux-specific metrics like `node_memory_MemTotal_bytes`. macOS node_exporter exports different metrics:
| Linux | macOS |
|-------|-------|
| node_memory_MemTotal_bytes | node_memory_total_bytes |
| node_memory_MemFree_bytes | node_memory_free_bytes |
| node_memory_Buffers_bytes | (not available) |
| node_memory_Cached_bytes | (not available) |
macOS has unique memory categories: Wired, Active, Compressed, Inactive, Free.
## Test plan
- [x] Dashboard deployed to indri via ansible
- [x] All panels showing data for indri
- [x] Instance selector works to switch between hosts
- [x] Disk and network device filters work
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: https://forge.tail8d86e.ts.net/eblume/blumeops/pulls/8
## Summary
- Adds Upload/Download Ratio stat panel with color thresholds (red < 0.5, yellow < 1, green >= 1)
- Adds Downloaded (Period) stat panel showing bytes downloaded in selected time range
- Adds Uploaded (Period) stat panel showing bytes uploaded in selected time range
Uses PromQL `increase()` on existing counter metrics - no new metrics collection needed.
## Test plan
- [x] Deployed to indri via `mise run provision-indri`
- [x] Grafana restarted successfully
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: https://forge.tail8d86e.ts.net/eblume/blumeops/pulls/6
- Add mise-tasks/provision-indri script to run ansible playbook
- Fix transmission_metrics launchctl load to be idempotent
- Update CLAUDE.md to reference mise run provision-indri
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Query torrent-get RPC to sum totalSize of all torrents
- Add transmission_torrents_size_bytes gauge metric
- Add "Total Torrent Size" timeseries panel to dashboard
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Transmission doesn't support HEAD requests, so use -i flag with sed to
parse only the HTTP headers (stopping at the blank line before body).
Also anchor grep pattern to line start to avoid matching HTML content.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add node_exporter ansible role to enable textfile collector
- Add transmission_metrics role with script and LaunchAgent
- Collects metrics every 60s via transmission RPC
- Writes to /opt/homebrew/var/node_exporter/textfile/transmission.prom
- Update grafana role to provision dashboards from files
- Add transmission.json dashboard with:
- Status indicator, torrent counts
- Transfer speeds, cumulative stats
- Time series graphs
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
- Homebrew's transmission-cli service uses /opt/homebrew/var/transmission/
not ~/.config/transmission-daemon/
- Add task to clean up old config directory
- Update zettelkasten with correct paths
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add new transmission ansible role using homebrew + brew services
- Configure transmission to download to ~/transmission with localhost-only RPC
- Modify kiwix role to use transmission for downloading ZIM archives via BitTorrent
- Add role dependency so running --tags kiwix auto-runs transmission
- Keep fallback to direct HTTP download when kiwix_use_transmission: false
- Symlink completed downloads from transmission dir to kiwix-tools dir
This reduces load on kiwix.org servers and allows downloads to continue
in the background without blocking ansible runs.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add full example with heredoc for multi-line descriptions and note
the difference from gh CLI (--description vs --body).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The LaunchAgent was failing because launchd runs with a minimal PATH
that doesn't include mise-installed binaries or homebrew. This adds:
- Use `mise x` wrapper to run borgmatic (survives version updates)
- Add /opt/homebrew/bin to PATH for borg dependency
- Add ansible tags to indri playbook for targeted role runs
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Configure grafana to use provisioned datasources instead of UI config
- Add prometheus datasource template managed by ansible
- Create minimal grafana.ini with custom provisioning path
- Move ansible_managed to group_vars (fixes deprecation warning)
- Add Remote Hosts and Git Workflow sections to CLAUDE.md
- Document feature branch workflow with tea CLI for PRs
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Manages installation and service via homebrew. Config at
/opt/homebrew/var/forgejo/custom/conf/app.ini contains secrets
and is not templated - backed up by borgmatic instead.
Includes check that fails with restore instructions if config missing.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Manages scheduled LaunchAgent for daily backups at 2:00 AM.
Borgmatic itself is installed via mise (pipx), not managed by ansible.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Configure ZIM archives as a variable list with download URLs
- Auto-download missing archives from download.kiwix.org
- Template plist to serve all configured archives
- Skip checksum calculation on stat for performance
- Add commented options for Gutenberg, iFixit, Stack Exchange, LibreTexts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>