diff --git a/vision-manager/manager.sh b/vision-manager/manager.sh index 8cb4f40..2313df9 100644 --- a/vision-manager/manager.sh +++ b/vision-manager/manager.sh @@ -64,6 +64,20 @@ crawl_running() { AND state = 'running';" 2>/dev/null || echo "ERR" } +# Is the analysis subsystem enabled in Admin → Settings? The runtime source of +# truth is the `app_settings` row (key='analysis'), NOT the .env seed. When +# analysis is OFF the worker drains nothing, so leftover pending jobs would +# otherwise keep vision pinned forever — we must gate on this too, not just the +# queue depth. Returns the literal 'true'/'false'; 'ERR' (or anything else) on +# query failure is treated as enabled by the caller (fail-open: never yank +# vision out from under a live analysis on a transient DB hiccup). +analysis_enabled() { + "${PSQL[@]}" -c \ + "SELECT CASE WHEN COALESCE((value->>'enabled')::boolean, false) + THEN 'true' ELSE 'false' END + FROM app_settings WHERE key = 'analysis';" 2>/dev/null || echo "ERR" +} + # ---- Memory pressure ------------------------------------------------------- # Host-wide used% from MemAvailable (NOT MemFree, which sits near "full" thanks # to page cache and would false-trigger constantly). /proc/meminfo in this @@ -151,6 +165,7 @@ log "started: container=$VISION_CONTAINER health=$VISION_HEALTH_URL poll=${POLL_ idle_for=0 # seconds the queue has been empty while vision is running up_for=0 # seconds vision has been running (for MAX_UPTIME backstop) crawl_deferred=0 # 1 while a start is held off by the crawl mutex (log on flip only) +analysis_off=0 # 1 while analysis is disabled in settings (log on flip only) mem_yield_until=0 # epoch deadline of the active memory-yield cooldown (0 = none) mem_inhibited=0 # 1 while starts are inhibited by memory pressure (log on flip only) @@ -169,6 +184,22 @@ while true; do ;; esac + # Gate on the runtime enable flag. If analysis is OFF, force the backlog to 0 + # so the normal idle path runs the STOP_DEBOUNCE timer and shuts vision down. + # We deliberately reuse the idle debounce (rather than stopping instantly) so + # a quick toggle off→on inside the debounce window keeps vision warm — no + # cold reload for a momentary flip. Fail-open: only a literal 'false' counts + # as disabled; 'ERR'/empty leaves the real queue depth in effect. + if [ "$(analysis_enabled)" = "false" ]; then + if [ "$analysis_off" != "1" ]; then + log "analysis disabled in settings — ignoring ${pending} queued job(s); idle timer will stop vision" + analysis_off=1 + fi + pending=0 + else + analysis_off=0 + fi + # Memory-yield gates (before the backlog logic). The active stop handles a # running vision the idle path can't; the inhibit forces pending=0 so the # existing idle-debounce path keeps it down (same shape as the crawl mutex). diff --git a/vision-manager/readonly-role.sql b/vision-manager/readonly-role.sql index 71f9ad4..8b42172 100644 --- a/vision-manager/readonly-role.sql +++ b/vision-manager/readonly-role.sql @@ -26,7 +26,11 @@ SELECT 'CREATE ROLE vision_manager LOGIN' -- (Re)set the password every run so rotating it is just a re-apply. ALTER ROLE vision_manager LOGIN PASSWORD :'pw'; --- CONNECT to the current database, and read-only on the one table we poll. +-- CONNECT to the current database, and read-only on the two tables we poll: +-- crawler_jobs (backlog depth + crawl-in-flight) and app_settings (the runtime +-- analysis-enabled flag — without this the manager can't tell that analysis was +-- turned off and would keep vision pinned on stale queued jobs). GRANT CONNECT ON DATABASE :"DBNAME" TO vision_manager; GRANT USAGE ON SCHEMA public TO vision_manager; GRANT SELECT ON crawler_jobs TO vision_manager; +GRANT SELECT ON app_settings TO vision_manager;