diff --git a/.gitea/workflows/npm-publish.yml b/.gitea/workflows/npm-publish.yml index ca6ad9e..f09e8c1 100644 --- a/.gitea/workflows/npm-publish.yml +++ b/.gitea/workflows/npm-publish.yml @@ -11,11 +11,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Publish to forge.black.lan npm registry + - name: Publish to cocotte ct-forge (verdaccio) registry run: | chmod +x bin/* - printf '//forge.black.lan/api/packages/lilith/npm/:_authToken=%s\n' "$NPM_TOKEN" > .npmrc - npm publish --registry http://forge.black.lan/api/packages/lilith/npm/ + printf '//134.199.243.61:4873/:_authToken=%s\n' "$NPM_TOKEN" > .npmrc + npm publish --registry http://134.199.243.61:4873/ rm .npmrc env: NPM_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/bin/bigdisk-mount-guard b/bin/bigdisk-mount-guard new file mode 100755 index 0000000..60276f3 --- /dev/null +++ b/bin/bigdisk-mount-guard @@ -0,0 +1,172 @@ +#!/bin/sh +# bigdisk-mount-guard — autorecover tool for the ~/_/bigdisk NFS mount +# (Apple Photos originals + other big storage on black). +# +# The mount is provided both via: +# - /etc/auto_nfs (autofs static): bigdisk -fstype=nfs,resvport black:/bigdisk +# - /etc/fstab (hard NFS): 10.0.0.11:/bigdisk /Users/natalie/_/bigdisk nfs resvport,rw,bg,hard,intr,tcp +# +# On sleep, network roam, WG flap or "no WiFi" location switches the handle often +# goes stale. autofs/fstab fails to re-trigger, Photos sees a dead symlink for +# originals and the library gets stuck (permission errors, "Repairing…", repeated +# password prompts for com.apple.library-repair). +# +# This tool autorecovers: +# 1. If storage IP unreachable, attempt to bring the WireGuard mesh (wg1) up. +# 2. If server reachable but mount wedged, force-umount + touch to re-trigger. +# 3. On success, run Photos-specific recovery (clean DB locks, restart daemons, +# restore canonical symlink if it had been pointed at a local stub). +# +# Designed to run as root LaunchDaemon (contrib plist). Also safe/useful by hand +# or from host-monitor/disk-guard style tools. +# +# No GNU timeout (uses perl alarm, always on macOS). +set -u + +# Support limited CLI for use as a tool: --status, --force, --help +case "${1:-}" in +--help|-h) + cat <<'EOF' +bigdisk-mount-guard [options] + + (no arg) normal check + autorecover (idempotent, for launchd or cron) + --status report only (0=healthy, 1=needs attention), no changes + --force force a full recovery cycle (useful after manual WG bring-up) + --help this message + +Runs as root (needs umount/mount/wg-quick). Logs to $LOG. +EOF + exit 0 + ;; +--status) + STATUS_ONLY=1 + ;; +--force) + FORCE=1 + ;; +"") ;; +*) + echo "unknown arg: $1" >&2 + exit 64 + ;; +esac + + +MP="/Users/natalie/_/bigdisk" +PROBE="${MP}/Photos" # a dir that only exists when the mount is live +SERVER_IP="10.0.0.11" # black; 'black' resolves here too, routed via LAN or WG +LOG="/var/log/bigdisk-mount-guard.log" +WG_IF="wg1" +WG_CONF="/Users/natalie/.wireguard/wg1.conf" +PHOTOS_LIB="/Users/natalie/Pictures/Photos Library.photoslibrary" + +log() { echo "$(/bin/date '+%Y-%m-%dT%H:%M:%S') $*" >> "$LOG" 2>/dev/null; } + +# macOS notification (works from root daemon for the console user) +notify() { + title="$1" + msg="$2" + /usr/bin/osascript -e "display notification \"$msg\" with title \"$title\" sound name \"Ping\"" 2>/dev/null || true +} + +# run "$@" but kill it after N seconds (first arg); returns the command's status, +# or 124 if it timed out / failed to exec. Uses perl's SIGALRM — always present on macOS. +bounded() { + _t=$1; shift + /usr/bin/perl -e 'alarm shift; exec @ARGV or exit 124' "$_t" "$@" +} + +ensure_wg() { + if /opt/homebrew/bin/wg show "$WG_IF" >/dev/null 2>&1; then + return 0 + fi + log "WG $WG_IF down; attempting wg-quick up to reach $SERVER_IP" + /opt/homebrew/bin/wg-quick down "$WG_CONF" >/dev/null 2>&1 || true + if /opt/homebrew/bin/wg-quick up "$WG_CONF" >>"$LOG" 2>&1; then + log "WG up succeeded; waiting for mesh routes" + sleep 8 + return 0 + else + log "wg-quick up failed" + return 1 + fi +} + +recover_photos() { + [ -d "$PHOTOS_LIB" ] || return 0 + log "Photos autorecover cleanup" + # stale locks often block open/repair after volume events + find "$PHOTOS_LIB/database" -name '*lock*' -type f -delete 2>/dev/null || true + # restart the user's analysis daemons so they see the freshly mounted originals + /usr/bin/sudo -u natalie /usr/bin/pkill -x photoanalysisd 2>/dev/null || true + /usr/bin/sudo -u natalie /usr/bin/pkill -f photolibraryd 2>/dev/null || true + # if a previous troubleshooting step pointed the symlink at a local stub, restore the real one + cur=$(readlink "$PHOTOS_LIB/originals" 2>/dev/null || true) + if echo "$cur" | grep -q 'Photos-stub\|stub/originals'; then + ln -sfn "${MP}/Photos/Photos Library.photoslibrary/originals" "$PHOTOS_LIB/originals" + log "restored canonical originals symlink" + fi + log "Photos cleanup done — relaunch Photos (hold Option+Command for Repair if it still whines)" + notify "bigdisk-mount-guard" "Photos library autorecovered (mount + cleanup)" +} + +# --- main --- + +if [ "${STATUS_ONLY:-0}" = 1 ]; then + if bounded 5 /usr/bin/stat "$PROBE" >/dev/null 2>&1; then + echo "OK: bigdisk probe $PROBE reachable" + exit 0 + else + echo "STALE: bigdisk probe $PROBE not reachable" + if /sbin/ping -c1 -t2 "$SERVER_IP" >/dev/null 2>&1; then + echo " server $SERVER_IP is up (mount wedged?)" + else + echo " server $SERVER_IP unreachable" + fi + exit 1 + fi +fi + +# Healthy path: probe stats quickly. Nothing (more) to do. +if [ "${FORCE:-0}" != 1 ] && bounded 8 /usr/bin/stat "$PROBE" >/dev/null 2>&1; then + exit 0 +fi + +# Path not reachable (or forced). Try to make the server reachable first. +if ! /sbin/ping -c1 -t3 "$SERVER_IP" >/dev/null 2>&1; then + log "probe failed; ${SERVER_IP} unreachable" + if ensure_wg; then + notify "bigdisk-mount-guard" "Brought up WG mesh to reach storage" + if ! /sbin/ping -c1 -t3 "$SERVER_IP" >/dev/null 2>&1; then + log "${SERVER_IP} still unreachable after WG attempt — leaving mount alone" + notify "bigdisk-mount-guard" "bigdisk offline (server unreachable after WG) — no action" + exit 0 + fi + # fallthrough to mount recovery now that mesh is up + else + log "WG recovery not possible — offline, leaving mount alone" + notify "bigdisk-mount-guard" "bigdisk offline (server unreachable) — no action taken" + exit 0 + fi +fi + +# Server reachable (possibly after WG recovery) but probe failed → wedged/stale mount. +log "probe failed but ${SERVER_IP} reachable — force-clearing wedged mount at ${MP}" +notify "bigdisk-mount-guard" "bigdisk mount wedged — force recovering now" +bounded 10 /sbin/umount -f "$MP" >/dev/null 2>&1 || true +# Kick automount to refresh static maps (safe/no-op if already good). +bounded 5 /usr/sbin/automount -vc >>"$LOG" 2>&1 || true +# Touch the path to (re)trigger autofs (or fstab bg reconnect). +bounded 15 /bin/ls "$MP" >/dev/null 2>&1 || true +sleep 3 + +if bounded 10 /usr/bin/stat "$PROBE" >/dev/null 2>&1; then + log "remount OK" + notify "bigdisk-mount-guard" "bigdisk mount recovered successfully" + recover_photos + exit 0 +fi + +log "remount still failing after force-clear" +notify "bigdisk-mount-guard" "bigdisk remount still failing (check logs)" +exit 1 diff --git a/bin/disk-guard b/bin/disk-guard new file mode 100755 index 0000000..97598d3 --- /dev/null +++ b/bin/disk-guard @@ -0,0 +1,81 @@ +#!/bin/sh +# disk-guard [--warn PCT] [--crit PCT] [--volume PATH] [--quiet] +# +# Check free space on the data volume and raise a macOS notification when it +# runs low. Meant to run unattended (launchd), but safe to run by hand. +# +# Unlike `disk-reclaim` (which only reports and is read-only), this is the +# *alarm*: it makes low disk visible BEFORE the volume hits ~97% full and +# things start failing. It never deletes anything. +# +# Flags: +# --warn PCT warn when free space drops below PCT percent (default 15) +# --crit PCT critical when free space drops below PCT percent (default 7) +# --volume PATH volume to check (default /System/Volumes/Data) +# --quiet no stdout; only notify + log (for launchd) +# +# Exit status: 0 ok, 1 warn, 2 critical. Always logs to +# ~/Library/Logs/disk-reclaim.log so boot snapshots and alarms share a trail. + +set -eu + +warn_pct=15 +crit_pct=7 +volume=/System/Volumes/Data +quiet=0 + +die() { echo "disk-guard: $*" >&2; exit 64; } + +while [ $# -gt 0 ]; do + case "$1" in + -h|--help) sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;; + --warn) [ $# -ge 2 ] || die "--warn needs a value"; warn_pct=$2; shift 2 ;; + --crit) [ $# -ge 2 ] || die "--crit needs a value"; crit_pct=$2; shift 2 ;; + --volume) [ $# -ge 2 ] || die "--volume needs a value"; volume=$2; shift 2 ;; + --quiet) quiet=1; shift ;; + *) die "unknown arg: $1" ;; + esac +done + +# df -k: portable. Columns: Filesystem 1024-blocks Used Available Capacity ... +# Available is field 4, in KiB. Compute free percent ourselves (Capacity is +# "used %" and rounds oddly on APFS shared containers). +read -r avail_kb total_kb </dev/null 2>&1 || true +} + +if [ "$free_pct" -lt "$crit_pct" ]; then + level=CRITICAL; rc=2 + notify "Disk critically full" "${avail_gb}G free (${free_pct}%) on $volume. Run: disk-reclaim" +elif [ "$free_pct" -lt "$warn_pct" ]; then + level=WARN; rc=1 + notify "Disk getting full" "${avail_gb}G free (${free_pct}%) on $volume. Run: disk-reclaim" +else + level=OK; rc=0 +fi + +echo "=== $stamp (guard) $level: ${avail_gb}G free (${free_pct}%) on $volume ===" >> "$log" + +# On WARN/CRIT, append a reclaim snapshot so the log shows what to delete. +if [ "$rc" -ne 0 ]; then + script_dir=$(cd "$(dirname "$0")" && pwd -P) + "$script_dir/disk-reclaim" "$HOME" --min 1G >> "$log" 2>&1 || true +fi + +[ "$quiet" = 1 ] || echo "$level: ${avail_gb}G free (${free_pct}%) on $volume" +exit "$rc" diff --git a/bin/disk-reclaim b/bin/disk-reclaim index f14f09e..b9e41b4 100755 --- a/bin/disk-reclaim +++ b/bin/disk-reclaim @@ -93,7 +93,7 @@ echo # stderr → /dev/null to silence permission-denied noise on system dirs. # shellcheck disable=SC2086 results=$( - find "$scan_root" -type d \( $expr \) -prune -print 2>/dev/null \ + find -x "$scan_root" -type d \( $expr \) -prune -print 2>/dev/null \ | while IFS= read -r dir; do kb=$(du -sk "$dir" 2>/dev/null | awk '{print $1}') [ -z "$kb" ] && continue diff --git a/contrib/com.natalie.bigdisk-mount-guard.plist b/contrib/com.natalie.bigdisk-mount-guard.plist new file mode 100644 index 0000000..58d9a7c --- /dev/null +++ b/contrib/com.natalie.bigdisk-mount-guard.plist @@ -0,0 +1,55 @@ + + + + + + Label + com.natalie.bigdisk-mount-guard + ProgramArguments + + /Users/natalie/Code/@scripts/session-tools/bin/bigdisk-mount-guard + + RunAtLoad + + StartInterval + 60 + WatchPaths + + /etc/resolv.conf + /Library/Preferences/SystemConfiguration/NetworkInterfaces.plist + + ProcessType + Background + LowPriorityIO + + StandardErrorPath + /var/log/bigdisk-mount-guard.err + + diff --git a/package.json b/package.json index 42905d4..882e6b9 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,6 @@ "bin" ], "publishConfig": { - "registry": "http://forge.black.lan/api/packages/lilith/npm/" + "registry": "http://134.199.243.61:4873/" } } diff --git a/waconsole b/waconsole new file mode 100755 index 0000000..903d34a --- /dev/null +++ b/waconsole @@ -0,0 +1,2 @@ +#!/bin/bash +exec "/Users/natalie/Code/@projects/@lilith/lilith-platform.live/users/transquinnftw/tools/whatsapp-lookup/console-tray/run.sh" "$@" diff --git a/walookup b/walookup new file mode 120000 index 0000000..0fe8c12 --- /dev/null +++ b/walookup @@ -0,0 +1 @@ +/Users/natalie/Code/@projects/@lilith/lilith-platform.live/users/transquinnftw/tools/whatsapp-lookup/lookup.sh \ No newline at end of file