From 5d30ab4a8ae1ff822eede2b25728393eeba03823 Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Mon, 3 Aug 2026 22:01:58 -0400 Subject: [PATCH] feat(search): sovereign SearXNG stack + mesh dashboard Search badge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sourceos-searxng-setup.sh (new): - Docker path: pulls searxng/searxng:latest → localhost:8888, skips if exists - Homebrew fallback: brew install searxng → localhost:8080 - Probes instance after setup; writes URL to ~/.local/state/sourceos/searxng-url com.sourceos.searxng.plist (new): - launchd agent: docker start sourceos-searxng at login - Activated via turtle-install-launchd --with-searxng turtle-web-search: - _searxng_url(): env var → state file → localhost probe (8888→8080) → searx.be - No public instance required when local SearXNG is running turtle-install-launchd: - --with-searxng flag appends com.sourceos.searxng to install list (off by default) - Updated usage string turtle-mesh-serve: - _searxng_alive(): same 4-level probe, 1s timeout - gather_state() includes searxng_ok bool - Dashboard status bar: Search badge (green=up / red=down) next to Noetica --- assets/sourceos/bin/turtle-install-launchd | 25 +++- assets/sourceos/bin/turtle-mesh-serve | 43 +++++-- assets/sourceos/bin/turtle-web-search | 36 +++++- .../launchd/com.sourceos.searxng.plist | 30 +++++ scripts/sourceos-searxng-setup.sh | 120 ++++++++++++++++++ 5 files changed, 237 insertions(+), 17 deletions(-) create mode 100644 assets/sourceos/launchd/com.sourceos.searxng.plist create mode 100755 scripts/sourceos-searxng-setup.sh diff --git a/assets/sourceos/bin/turtle-install-launchd b/assets/sourceos/bin/turtle-install-launchd index 2bbee4ae043..4242e2a4018 100755 --- a/assets/sourceos/bin/turtle-install-launchd +++ b/assets/sourceos/bin/turtle-install-launchd @@ -5,10 +5,14 @@ # com.sourceos.turtle-mesh-push — GCS mesh sync every 5 minutes # com.sourceos.turtle-mesh-serve — local mesh dashboard on :7788 # +# Optionally installs (requires --with-searxng): +# com.sourceos.searxng — start sovereign SearXNG container at login +# # Usage: -# turtle-install-launchd # install both -# turtle-install-launchd --unload # stop + unload both -# turtle-install-launchd --status # show launchctl status +# turtle-install-launchd # install mesh agents +# turtle-install-launchd --with-searxng # also install SearXNG agent +# turtle-install-launchd --unload # stop + unload all loaded agents +# turtle-install-launchd --status # show launchctl status set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -18,20 +22,26 @@ LAUNCH_AGENTS="$HOME/Library/LaunchAgents" LABELS=(com.sourceos.turtle-mesh-push com.sourceos.turtle-mesh-serve) usage() { - echo "Usage: turtle-install-launchd [--unload|--status]" + echo "Usage: turtle-install-launchd [--with-searxng] [--unload|--status]" } status_mode=false unload_mode=false +with_searxng=false for arg in "$@"; do case "$arg" in - --status) status_mode=true ;; - --unload) unload_mode=true ;; + --status) status_mode=true ;; + --unload) unload_mode=true ;; + --with-searxng) with_searxng=true ;; -h|--help) usage; exit 0 ;; *) echo "Unknown arg: $arg" >&2; usage >&2; exit 1 ;; esac done +if $with_searxng; then + LABELS+=(com.sourceos.searxng) +fi + if $status_mode; then for label in "${LABELS[@]}"; do echo -n " $label: " @@ -72,3 +82,6 @@ done echo "" echo " Mesh dashboard → http://localhost:7788" echo " GCS sync → every 5 minutes (requires SOURCEOS_GCS_BUCKET env in ~/.zshrc)" +if $with_searxng; then + echo " SearXNG → http://localhost:8888 (container must exist; run sourceos-searxng-setup.sh first)" +fi diff --git a/assets/sourceos/bin/turtle-mesh-serve b/assets/sourceos/bin/turtle-mesh-serve index d2c29d4b01b..10015716d4d 100755 --- a/assets/sourceos/bin/turtle-mesh-serve +++ b/assets/sourceos/bin/turtle-mesh-serve @@ -26,6 +26,7 @@ import queue import sys import threading import time +import urllib.request from http.server import BaseHTTPRequestHandler, HTTPServer from pathlib import Path @@ -60,6 +61,23 @@ def load_jsonl_tail(path: Path, n: int = 50) -> list[dict]: return items[-n:] +def _searxng_alive() -> bool: + state_file = Path(os.getenv("XDG_STATE_HOME", str(Path.home() / ".local/state"))) / "sourceos" / "searxng-url" + candidates = [] + if state_file.exists(): + url = state_file.read_text().strip().rstrip("/") + if url: + candidates.append(url) + candidates += ["http://localhost:8888", "http://localhost:8080"] + for url in candidates: + try: + urllib.request.urlopen(f"{url}/healthz", timeout=1) + return True + except Exception: + pass + return False + + def gather_state() -> dict: mesh = load_jsonl_tail(MESH_DIR / "context.jsonl", 60) active = load_json(MESH_DIR / "active.json") @@ -67,6 +85,7 @@ def gather_state() -> dict: pr = load_json(STATUS_DIR / "pr.json") noetica= load_json(STATUS_DIR / "noetica.json") board = load_json(STATUS_DIR / "board.json") + searxng_ok = _searxng_alive() bb_cands: list[dict] = [] cand_path = BB_SUPPORT / "memory" / "candidates.jsonl" @@ -84,15 +103,16 @@ def gather_state() -> dict: notes.append({"name": p.name, "mtime": p.stat().st_mtime}) return { - "ts": datetime.datetime.now().isoformat(), - "active": active, - "mesh": mesh, - "ci": ci, - "pr": pr, - "noetica": noetica, - "board": board, - "bb_cands": bb_cands, - "notes": notes, + "ts": datetime.datetime.now().isoformat(), + "active": active, + "mesh": mesh, + "ci": ci, + "pr": pr, + "noetica": noetica, + "board": board, + "bb_cands": bb_cands, + "notes": notes, + "searxng_ok": searxng_ok, } @@ -227,6 +247,7 @@ h2{font-size:12px;color:var(--dim);text-transform:uppercase;letter-spacing:.08em