mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-08-09 12:16:01 +02:00
Let agents temporarily override native map layer visibility.
This commit is contained in:
@@ -381,6 +381,50 @@ async def api_refresh_layer_feed(request: Request, layer_id: str):
|
||||
return {"ok": True, "layer": updated}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Native map layer overrides — additive, transient, agent-driven.
|
||||
#
|
||||
# These are the DATA LAYERS toggles, not the pin layers above. An override
|
||||
# switches an overlay on without touching the operator's own toggle state, and
|
||||
# lapses on its own after the TTL. Agents holding an overlay open should re-PUT
|
||||
# on their refresh tick rather than sending a long TTL.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class LayerOverrideUpdate(BaseModel):
|
||||
layers: dict[str, bool]
|
||||
ttl_seconds: float = 300.0
|
||||
|
||||
|
||||
@router.put("/api/ai/layer-overrides", dependencies=[Depends(require_openclaw_or_local)])
|
||||
@limiter.limit("30/minute")
|
||||
async def put_layer_overrides(request: Request, body: LayerOverrideUpdate):
|
||||
"""Replace the override map. Returns which keys were accepted and ignored."""
|
||||
from services.fetchers._store import set_layer_overrides
|
||||
|
||||
accepted = set_layer_overrides(body.layers, body.ttl_seconds)
|
||||
ignored = sorted(set(body.layers) - set(accepted))
|
||||
return {"ok": True, "overrides": accepted, "ignored": ignored}
|
||||
|
||||
|
||||
@router.get("/api/ai/layer-overrides", dependencies=[Depends(require_openclaw_or_local)])
|
||||
@limiter.limit("60/minute")
|
||||
async def read_layer_overrides(request: Request):
|
||||
"""Return the overrides that are currently live."""
|
||||
from services.fetchers._store import get_layer_overrides
|
||||
|
||||
return {"ok": True, "overrides": get_layer_overrides()}
|
||||
|
||||
|
||||
@router.delete("/api/ai/layer-overrides", dependencies=[Depends(require_openclaw_or_local)])
|
||||
@limiter.limit("30/minute")
|
||||
async def delete_layer_overrides(request: Request):
|
||||
"""Drop all overrides, restoring the operator's own layer state."""
|
||||
from services.fetchers._store import clear_layer_overrides
|
||||
|
||||
clear_layer_overrides()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Agent Actions endpoint — frontend polls this for UI commands from the agent
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
+29
-4
@@ -566,6 +566,18 @@ def _run_prediction_markets_disable() -> None:
|
||||
logger.warning("Prediction markets disable cleanup failed: %s", e)
|
||||
|
||||
|
||||
@router.get("/api/layers", dependencies=[Depends(require_local_operator)])
|
||||
async def get_layers(request: Request):
|
||||
"""Report operator layer state plus any live overrides.
|
||||
|
||||
The UI polls this so the DATA LAYERS toggles can show an overlay that an
|
||||
agent switched on. ``layers`` is the operator's own state and is what the
|
||||
browser persists; ``overrides`` is transient and must not be saved.
|
||||
"""
|
||||
from services.fetchers._store import active_layers, get_layer_overrides
|
||||
return {"layers": dict(active_layers), "overrides": get_layer_overrides()}
|
||||
|
||||
|
||||
@router.post("/api/layers", dependencies=[Depends(require_local_operator)])
|
||||
@limiter.limit("30/minute")
|
||||
async def update_layers(update: LayerUpdate, request: Request):
|
||||
@@ -668,11 +680,15 @@ async def bootstrap_critical(request: Request):
|
||||
if request.headers.get("if-none-match") == etag:
|
||||
return Response(status_code=304, headers={"ETag": etag, "Cache-Control": "no-cache"})
|
||||
from services.fetchers._store import (
|
||||
active_layers,
|
||||
effective_layers,
|
||||
get_latest_data_subset_refs,
|
||||
get_source_timestamps_snapshot,
|
||||
)
|
||||
|
||||
# Rebind the local name to the override-merged map. Every layer filter
|
||||
# below reads this name, so one merge here covers all of them.
|
||||
active_layers = effective_layers()
|
||||
|
||||
def _build() -> dict:
|
||||
d = get_latest_data_subset_refs(
|
||||
"last_updated", "commercial_flights", "military_flights", "private_flights",
|
||||
@@ -769,14 +785,17 @@ def _try_build_fast_delta(
|
||||
) -> dict | None:
|
||||
"""Return a delta payload, or None to fall back to a full snapshot."""
|
||||
from services.fetchers._store import (
|
||||
active_layers,
|
||||
compute_layer_row_delta,
|
||||
effective_layers,
|
||||
get_data_version,
|
||||
get_layer_versions,
|
||||
get_latest_data_subset_refs,
|
||||
get_source_timestamps_snapshot,
|
||||
)
|
||||
|
||||
# Rebind the local name to the override-merged map (see bootstrap_critical).
|
||||
active_layers = effective_layers()
|
||||
|
||||
server_lv = get_layer_versions()
|
||||
deltas: dict[str, Any] = {}
|
||||
for key in _DELTA_FAST_KEYS:
|
||||
@@ -894,7 +913,10 @@ async def live_data_fast(
|
||||
etag = _current_etag(prefix=("fast|initial|" if initial else "fast|full|") + bbox_suffix.lstrip("|") + ("|" if bbox_suffix else ""))
|
||||
if request.headers.get("if-none-match") == etag:
|
||||
return Response(status_code=304, headers={"ETag": etag, "Cache-Control": "no-cache"})
|
||||
from services.fetchers._store import (active_layers, get_latest_data_subset_refs, get_source_timestamps_snapshot)
|
||||
from services.fetchers._store import (effective_layers, get_latest_data_subset_refs, get_source_timestamps_snapshot)
|
||||
|
||||
# Rebind the local name to the override-merged map (see bootstrap_critical).
|
||||
active_layers = effective_layers()
|
||||
|
||||
def _build() -> dict:
|
||||
d = get_latest_data_subset_refs(
|
||||
@@ -959,7 +981,10 @@ async def live_data_slow(
|
||||
etag = _current_etag(prefix="slow|full|" + bbox_suffix.lstrip("|") + ("|" if bbox_suffix else ""))
|
||||
if request.headers.get("if-none-match") == etag:
|
||||
return Response(status_code=304, headers={"ETag": etag, "Cache-Control": "no-cache"})
|
||||
from services.fetchers._store import (active_layers, get_latest_data_subset_refs, get_source_timestamps_snapshot)
|
||||
from services.fetchers._store import (effective_layers, get_latest_data_subset_refs, get_source_timestamps_snapshot)
|
||||
|
||||
# Rebind the local name to the override-merged map (see bootstrap_critical).
|
||||
active_layers = effective_layers()
|
||||
|
||||
def _build() -> dict:
|
||||
d = get_latest_data_subset_refs(
|
||||
|
||||
@@ -7,6 +7,7 @@ Every fetcher imports from here instead of maintaining its own copy.
|
||||
import copy
|
||||
import threading
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, TypedDict
|
||||
|
||||
@@ -505,6 +506,69 @@ active_layers: dict[str, bool] = {
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Layer overrides — additive, agent-driven, never persisted.
|
||||
# An automation (e.g. a hotspot daemon) can switch an overlay on for a while
|
||||
# without touching the operator's own toggles. Overrides merge ON TOP of
|
||||
# active_layers for reads; active_layers itself is only ever written by the
|
||||
# operator via POST /api/layers. The whole map shares one expiry, evaluated
|
||||
# lazily on read so no background task is needed. When it lapses the operator's
|
||||
# own view returns with no save/restore bookkeeping.
|
||||
# ---------------------------------------------------------------------------
|
||||
_MAX_OVERRIDE_TTL_S = 3600.0
|
||||
|
||||
layer_overrides: dict[str, bool] = {}
|
||||
_layer_overrides_expires_at: float = 0.0
|
||||
|
||||
|
||||
def get_layer_overrides() -> dict[str, bool]:
|
||||
"""Return the live overrides, or {} once the TTL has lapsed."""
|
||||
global _layer_overrides_expires_at
|
||||
if not layer_overrides:
|
||||
return {}
|
||||
if time.monotonic() >= _layer_overrides_expires_at:
|
||||
layer_overrides.clear()
|
||||
_layer_overrides_expires_at = 0.0
|
||||
bump_active_layers_version()
|
||||
return {}
|
||||
return dict(layer_overrides)
|
||||
|
||||
|
||||
def set_layer_overrides(overrides: dict[str, bool], ttl_seconds: float) -> dict[str, bool]:
|
||||
"""Replace the override map, returning the entries that were accepted.
|
||||
|
||||
Keys that are not real layers are dropped so a typo cannot silently do
|
||||
nothing — the caller compares the return value against what it sent.
|
||||
"""
|
||||
global _layer_overrides_expires_at
|
||||
ttl = max(0.0, min(float(ttl_seconds), _MAX_OVERRIDE_TTL_S))
|
||||
accepted = {k: bool(v) for k, v in overrides.items() if k in active_layers}
|
||||
layer_overrides.clear()
|
||||
layer_overrides.update(accepted)
|
||||
_layer_overrides_expires_at = time.monotonic() + ttl if accepted else 0.0
|
||||
bump_active_layers_version()
|
||||
return accepted
|
||||
|
||||
|
||||
def clear_layer_overrides() -> None:
|
||||
"""Drop all overrides, restoring the operator's own layer state."""
|
||||
global _layer_overrides_expires_at
|
||||
if layer_overrides:
|
||||
layer_overrides.clear()
|
||||
_layer_overrides_expires_at = 0.0
|
||||
bump_active_layers_version()
|
||||
|
||||
|
||||
def effective_layers() -> dict[str, bool]:
|
||||
"""Operator layer state merged with any live overrides."""
|
||||
overrides = get_layer_overrides()
|
||||
return {**active_layers, **overrides} if overrides else dict(active_layers)
|
||||
|
||||
|
||||
def is_any_active(*layer_names: str) -> bool:
|
||||
"""Return True if any of the given layer names is currently active."""
|
||||
return any(active_layers.get(name, True) for name in layer_names)
|
||||
overrides = get_layer_overrides()
|
||||
return any(
|
||||
overrides.get(name, active_layers.get(name, True))
|
||||
for name in layer_names
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user