Improve v0.9.7 startup and runtime reliability

Prioritize cached first-paint data, defer heavyweight feed synthesis, make MeshChat activation explicit, improve CCTV media handling, and tighten desktop runtime packaging filters.
This commit is contained in:
BigBodyCobain
2026-05-02 17:31:54 -06:00
parent 08810f2537
commit e1060193d0
14 changed files with 699 additions and 153 deletions
+8 -2
View File
@@ -402,7 +402,9 @@ async def require_openclaw_or_local(request: Request):
# Startup validators # Startup validators
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
_KNOWN_COMPROMISED_PEER_PUSH_SECRET = "Mv63UvLfwqOEVWeRBXjA8MtFl2nEkkhUlLYVHiX1Zzo" _KNOWN_COMPROMISED_PEER_PUSH_SECRET_SHA256 = (
"be05bc75350d6e5d2e154e969c4dfc14bab1e48a9661c64ab7a331e0aa96aea7"
)
def _validate_admin_startup() -> None: def _validate_admin_startup() -> None:
@@ -492,7 +494,11 @@ def _validate_peer_push_secret() -> None:
secret = os.environ.get("MESH_PEER_PUSH_SECRET", "").strip() secret = os.environ.get("MESH_PEER_PUSH_SECRET", "").strip()
# Replace the known-compromised testnet default automatically # Replace the known-compromised testnet default automatically
if secret == _KNOWN_COMPROMISED_PEER_PUSH_SECRET: if (
secret
and _hashlib_mod.sha256(secret.encode("utf-8")).hexdigest()
== _KNOWN_COMPROMISED_PEER_PUSH_SECRET_SHA256
):
logger.warning( logger.warning(
"MESH_PEER_PUSH_SECRET was the publicly-known testnet default — " "MESH_PEER_PUSH_SECRET was the publicly-known testnet default — "
"auto-generating a secure replacement." "auto-generating a secure replacement."
+81 -14
View File
@@ -2175,19 +2175,26 @@ async def lifespan(app: FastAPI):
# Only the primary backend supervises Wormhole. The Wormhole process itself # Only the primary backend supervises Wormhole. The Wormhole process itself
# runs this same app in MESH_ONLY mode and must not recurse into spawning. # runs this same app in MESH_ONLY mode and must not recurse into spawning.
if not _MESH_ONLY: if not _MESH_ONLY:
try: def _startup_wormhole_runtime():
from services.wormhole_supervisor import get_wormhole_state, sync_wormhole_with_settings try:
from services.wormhole_supervisor import get_wormhole_state, sync_wormhole_with_settings
sync_wormhole_with_settings() sync_wormhole_with_settings()
_resume_private_delivery_background_work( _resume_private_delivery_background_work(
current_tier=_current_private_lane_tier(get_wormhole_state()), current_tier=_current_private_lane_tier(get_wormhole_state()),
reason="startup_resume", reason="startup_resume",
) )
_refresh_lookup_handle_rotation_background(reason="startup_resume") _refresh_lookup_handle_rotation_background(reason="startup_resume")
privacy_prewarm_service.ensure_started() privacy_prewarm_service.ensure_started()
privacy_prewarm_service.run_scheduled_once(reason="startup_resume") privacy_prewarm_service.run_scheduled_once(reason="startup_resume")
except Exception as e: except Exception as e:
logger.warning(f"Wormhole supervisor failed to sync: {e}") logger.warning(f"Wormhole supervisor failed to sync: {e}")
threading.Thread(
target=_startup_wormhole_runtime,
daemon=True,
name="wormhole-startup-sync",
).start()
try: try:
from services.mesh.mesh_hashchain import register_public_event_append_hook from services.mesh.mesh_hashchain import register_public_event_append_hook
@@ -7660,6 +7667,13 @@ _CCTV_PROXY_ALLOWED_HOSTS = {
"infocar.dgt.es", # Spain DGT "infocar.dgt.es", # Spain DGT
"informo.madrid.es", # Madrid "informo.madrid.es", # Madrid
"www.windy.com", "www.windy.com",
"imgproxy.windy.com", # Windy preview image CDN
"www.lakecountypassage.com", # Illinois Lake County PASSAGE snapshots
"webcam.forkswa.com", # WSDOT partner public camera
"webcam.sunmountainlodge.com", # WSDOT partner public camera
"www.nps.gov", # WSDOT-linked Mount Rainier camera
"home.lewiscounty.com", # WSDOT partner public camera
"www.seattle.gov", # Seattle traffic camera media linked from WSDOT
} }
@@ -7785,6 +7799,16 @@ def _cctv_proxy_profile_for_url(target_url: str) -> _CCTVProxyProfile:
cache_seconds=30, cache_seconds=30,
headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"}, headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"},
) )
if host == "www.lakecountypassage.com":
return _CCTVProxyProfile(
name="lake-county-passage",
timeout=(5.0, 12.0),
cache_seconds=30,
headers={
"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
"Referer": "https://www.lakecountypassage.com/",
},
)
if host in {"mdotjboss.state.mi.us", "micamerasimages.net"}: if host in {"mdotjboss.state.mi.us", "micamerasimages.net"}:
return _CCTVProxyProfile( return _CCTVProxyProfile(
name="michigan-dot", name="michigan-dot",
@@ -7847,11 +7871,27 @@ def _cctv_proxy_profile_for_url(target_url: str) -> _CCTVProxyProfile:
"Referer": "https://informo.madrid.es/", "Referer": "https://informo.madrid.es/",
}, },
) )
if host == "www.windy.com": if host in {"www.windy.com", "imgproxy.windy.com"}:
return _CCTVProxyProfile( return _CCTVProxyProfile(
name="windy-webcams", name="windy-webcams",
timeout=(5.0, 12.0), timeout=(5.0, 12.0),
cache_seconds=60, cache_seconds=60,
headers={
"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
"Referer": "https://www.windy.com/",
},
)
if host in {
"webcam.forkswa.com",
"webcam.sunmountainlodge.com",
"www.nps.gov",
"home.lewiscounty.com",
"www.seattle.gov",
}:
return _CCTVProxyProfile(
name="wsdot-partner",
timeout=(5.0, 12.0),
cache_seconds=30,
headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"}, headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"},
) )
return _CCTVProxyProfile( return _CCTVProxyProfile(
@@ -7895,6 +7935,30 @@ def _cctv_response_headers(resp, cache_seconds: int, include_length: bool = True
return headers return headers
def _infer_cctv_media_type_from_url(target_url: str, content_type: str) -> str:
from urllib.parse import urlparse
normalized_type = str(content_type or "").split(";", 1)[0].strip().lower()
if normalized_type and normalized_type not in {"application/octet-stream", "binary/octet-stream"}:
return content_type
path = str(urlparse(target_url).path or "").lower()
if path.endswith((".jpg", ".jpeg")):
return "image/jpeg"
if path.endswith(".png"):
return "image/png"
if path.endswith(".gif"):
return "image/gif"
if path.endswith(".webp"):
return "image/webp"
if path.endswith(".mp4"):
return "video/mp4"
if path.endswith(".webm"):
return "video/webm"
if path.endswith(".m3u8"):
return "application/vnd.apple.mpegurl"
return content_type or "application/octet-stream"
def _fetch_cctv_upstream_response(request: Request, target_url: str, profile: _CCTVProxyProfile): def _fetch_cctv_upstream_response(request: Request, target_url: str, profile: _CCTVProxyProfile):
import requests as _req import requests as _req
@@ -7928,7 +7992,10 @@ def _proxy_cctv_media_response(request: Request, target_url: str):
profile = _cctv_proxy_profile_for_url(target_url) profile = _cctv_proxy_profile_for_url(target_url)
resp = _fetch_cctv_upstream_response(request, target_url, profile) resp = _fetch_cctv_upstream_response(request, target_url, profile)
content_type = resp.headers.get("Content-Type", "application/octet-stream") content_type = _infer_cctv_media_type_from_url(
target_url,
resp.headers.get("Content-Type", "application/octet-stream"),
)
is_hls_playlist = ( is_hls_playlist = (
".m3u8" in str(parsed.path or "").lower() ".m3u8" in str(parsed.path or "").lower()
or "mpegurl" in content_type.lower() or "mpegurl" in content_type.lower()
+67 -22
View File
@@ -11,6 +11,8 @@ logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
_CCTV_PROXY_CONNECT_TIMEOUT_S = 2.0
_CCTV_PROXY_ALLOWED_HOSTS = { _CCTV_PROXY_ALLOWED_HOSTS = {
"s3-eu-west-1.amazonaws.com", "s3-eu-west-1.amazonaws.com",
"jamcams.tfl.gov.uk", "jamcams.tfl.gov.uk",
@@ -46,13 +48,20 @@ _CCTV_PROXY_ALLOWED_HOSTS = {
"infocar.dgt.es", "infocar.dgt.es",
"informo.madrid.es", "informo.madrid.es",
"www.windy.com", "www.windy.com",
"imgproxy.windy.com",
"www.lakecountypassage.com",
"webcam.forkswa.com",
"webcam.sunmountainlodge.com",
"www.nps.gov",
"home.lewiscounty.com",
"www.seattle.gov",
} }
@dataclass(frozen=True) @dataclass(frozen=True)
class _CCTVProxyProfile: class _CCTVProxyProfile:
name: str name: str
timeout: tuple = (5.0, 10.0) timeout: tuple = (_CCTV_PROXY_CONNECT_TIMEOUT_S, 8.0)
cache_seconds: int = 30 cache_seconds: int = 30
headers: dict = field(default_factory=dict) headers: dict = field(default_factory=dict)
@@ -80,69 +89,78 @@ def _cctv_proxy_profile_for_url(target_url: str) -> _CCTVProxyProfile:
path = str(parsed.path or "").strip().lower() path = str(parsed.path or "").strip().lower()
if host in {"jamcams.tfl.gov.uk", "s3-eu-west-1.amazonaws.com"}: if host in {"jamcams.tfl.gov.uk", "s3-eu-west-1.amazonaws.com"}:
return _CCTVProxyProfile(name="tfl-jamcam", timeout=(5.0, 20.0), cache_seconds=15, return _CCTVProxyProfile(name="tfl-jamcam", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 20.0), cache_seconds=15,
headers={"Accept": "video/mp4,image/avif,image/webp,image/apng,image/*,*/*;q=0.8", "Referer": "https://tfl.gov.uk/"}) headers={"Accept": "video/mp4,image/avif,image/webp,image/apng,image/*,*/*;q=0.8", "Referer": "https://tfl.gov.uk/"})
if host == "images.data.gov.sg": if host == "images.data.gov.sg":
return _CCTVProxyProfile(name="lta-singapore", timeout=(5.0, 10.0), cache_seconds=30, return _CCTVProxyProfile(name="lta-singapore", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 10.0), cache_seconds=30,
headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"}) headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"})
if host == "cctv.austinmobility.io": if host == "cctv.austinmobility.io":
return _CCTVProxyProfile(name="austin-mobility", timeout=(5.0, 8.0), cache_seconds=15, return _CCTVProxyProfile(name="austin-mobility", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 8.0), cache_seconds=15,
headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
"Referer": "https://data.mobility.austin.gov/", "Origin": "https://data.mobility.austin.gov"}) "Referer": "https://data.mobility.austin.gov/", "Origin": "https://data.mobility.austin.gov"})
if host == "webcams.nyctmc.org": if host == "webcams.nyctmc.org":
return _CCTVProxyProfile(name="nyc-dot", timeout=(5.0, 10.0), cache_seconds=15, return _CCTVProxyProfile(name="nyc-dot", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 10.0), cache_seconds=15,
headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"}) headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"})
if host in {"cwwp2.dot.ca.gov", "wzmedia.dot.ca.gov"}: if host in {"cwwp2.dot.ca.gov", "wzmedia.dot.ca.gov"}:
return _CCTVProxyProfile(name="caltrans", timeout=(5.0, 15.0), cache_seconds=15, return _CCTVProxyProfile(name="caltrans", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 15.0), cache_seconds=15,
headers={"Accept": "application/vnd.apple.mpegurl,application/x-mpegURL,video/*,image/*,*/*;q=0.8", headers={"Accept": "application/vnd.apple.mpegurl,application/x-mpegURL,video/*,image/*,*/*;q=0.8",
"Referer": "https://cwwp2.dot.ca.gov/"}) "Referer": "https://cwwp2.dot.ca.gov/"})
if host in {"images.wsdot.wa.gov", "olypen.com", "flyykm.com", "cam.pangbornairport.com"}: if host in {"images.wsdot.wa.gov", "olypen.com", "flyykm.com", "cam.pangbornairport.com"}:
return _CCTVProxyProfile(name="wsdot", timeout=(5.0, 12.0), cache_seconds=30, return _CCTVProxyProfile(name="wsdot", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 12.0), cache_seconds=30,
headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"}) headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"})
if host in {"www.lakecountypassage.com", "webcam.forkswa.com", "webcam.sunmountainlodge.com", "home.lewiscounty.com", "www.seattle.gov"}:
return _CCTVProxyProfile(name="regional-cctv-image", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 10.0), cache_seconds=45,
headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
"Referer": f"https://{host}/"})
if host == "www.nps.gov":
return _CCTVProxyProfile(name="nps-webcam", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 10.0), cache_seconds=60,
headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
"Referer": "https://www.nps.gov/"})
if host in {"navigator-c2c.dot.ga.gov", "navigator-c2c.ga.gov", "navigator-csc.dot.ga.gov"}: if host in {"navigator-c2c.dot.ga.gov", "navigator-c2c.ga.gov", "navigator-csc.dot.ga.gov"}:
read_timeout = 18.0 if "/snapshots/" in path else 12.0 read_timeout = 18.0 if "/snapshots/" in path else 12.0
return _CCTVProxyProfile(name="gdot-snapshot", timeout=(5.0, read_timeout), cache_seconds=15, return _CCTVProxyProfile(name="gdot-snapshot", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, read_timeout), cache_seconds=15,
headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
"Referer": "http://navigator-c2c.dot.ga.gov/"}) "Referer": "http://navigator-c2c.dot.ga.gov/"})
if host == "511ga.org": if host == "511ga.org":
return _CCTVProxyProfile(name="gdot-511ga-image", timeout=(5.0, 12.0), cache_seconds=15, return _CCTVProxyProfile(name="gdot-511ga-image", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 12.0), cache_seconds=15,
headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
"Referer": "https://511ga.org/cctv"}) "Referer": "https://511ga.org/cctv"})
if host.startswith("vss") and host.endswith("dot.ga.gov"): if host.startswith("vss") and host.endswith("dot.ga.gov"):
return _CCTVProxyProfile(name="gdot-hls", timeout=(5.0, 20.0), cache_seconds=10, return _CCTVProxyProfile(name="gdot-hls", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 20.0), cache_seconds=10,
headers={"Accept": "application/vnd.apple.mpegurl,application/x-mpegURL,video/*,*/*;q=0.8", headers={"Accept": "application/vnd.apple.mpegurl,application/x-mpegURL,video/*,*/*;q=0.8",
"Referer": "http://navigator-c2c.dot.ga.gov/"}) "Referer": "http://navigator-c2c.dot.ga.gov/"})
if host in {"gettingaroundillinois.com", "cctv.travelmidwest.com"}: if host in {"gettingaroundillinois.com", "cctv.travelmidwest.com"}:
return _CCTVProxyProfile(name="illinois-dot", timeout=(5.0, 12.0), cache_seconds=30, return _CCTVProxyProfile(name="illinois-dot", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 12.0), cache_seconds=30,
headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"}) headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"})
if host in {"mdotjboss.state.mi.us", "micamerasimages.net"}: if host in {"mdotjboss.state.mi.us", "micamerasimages.net"}:
return _CCTVProxyProfile(name="michigan-dot", timeout=(5.0, 12.0), cache_seconds=30, return _CCTVProxyProfile(name="michigan-dot", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 12.0), cache_seconds=30,
headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
"Referer": "https://mdotjboss.state.mi.us/"}) "Referer": "https://mdotjboss.state.mi.us/"})
if host in {"publicstreamer1.cotrip.org", "publicstreamer2.cotrip.org", if host in {"publicstreamer1.cotrip.org", "publicstreamer2.cotrip.org",
"publicstreamer3.cotrip.org", "publicstreamer4.cotrip.org"}: "publicstreamer3.cotrip.org", "publicstreamer4.cotrip.org"}:
return _CCTVProxyProfile(name="cotrip-hls", timeout=(5.0, 20.0), cache_seconds=10, return _CCTVProxyProfile(name="cotrip-hls", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 20.0), cache_seconds=10,
headers={"Accept": "application/vnd.apple.mpegurl,application/x-mpegURL,video/*,*/*;q=0.8", headers={"Accept": "application/vnd.apple.mpegurl,application/x-mpegURL,video/*,*/*;q=0.8",
"Referer": "https://www.cotrip.org/"}) "Referer": "https://www.cotrip.org/"})
if host == "cocam.carsprogram.org": if host == "cocam.carsprogram.org":
return _CCTVProxyProfile(name="cotrip-preview", timeout=(5.0, 12.0), cache_seconds=20, return _CCTVProxyProfile(name="cotrip-preview", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 12.0), cache_seconds=20,
headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
"Referer": "https://www.cotrip.org/"}) "Referer": "https://www.cotrip.org/"})
if host in {"tripcheck.com", "www.tripcheck.com"}: if host in {"tripcheck.com", "www.tripcheck.com"}:
return _CCTVProxyProfile(name="odot-tripcheck", timeout=(5.0, 12.0), cache_seconds=30, return _CCTVProxyProfile(name="odot-tripcheck", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 12.0), cache_seconds=30,
headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"}) headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"})
if host == "infocar.dgt.es": if host == "infocar.dgt.es":
return _CCTVProxyProfile(name="dgt-spain", timeout=(5.0, 8.0), cache_seconds=60, return _CCTVProxyProfile(name="dgt-spain", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 8.0), cache_seconds=60,
headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
"Referer": "https://infocar.dgt.es/"}) "Referer": "https://infocar.dgt.es/"})
if host == "informo.madrid.es": if host == "informo.madrid.es":
return _CCTVProxyProfile(name="madrid-city", timeout=(5.0, 12.0), cache_seconds=30, return _CCTVProxyProfile(name="madrid-city", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 12.0), cache_seconds=30,
headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
"Referer": "https://informo.madrid.es/"}) "Referer": "https://informo.madrid.es/"})
if host == "www.windy.com": if host in {"www.windy.com", "imgproxy.windy.com"}:
return _CCTVProxyProfile(name="windy-webcams", timeout=(5.0, 12.0), cache_seconds=60, return _CCTVProxyProfile(name="windy-webcams", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 12.0), cache_seconds=60,
headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"}) headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
return _CCTVProxyProfile(name="generic-cctv", timeout=(5.0, 10.0), cache_seconds=30, "Referer": "https://www.windy.com/"})
return _CCTVProxyProfile(name="generic-cctv", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 8.0), cache_seconds=30,
headers={"Accept": "*/*"}) headers={"Accept": "*/*"})
@@ -221,13 +239,40 @@ def _rewrite_cctv_hls_playlist(base_url: str, body: str) -> str:
return "\n".join(rewritten_lines) + ("\n" if body.endswith("\n") else "") return "\n".join(rewritten_lines) + ("\n" if body.endswith("\n") else "")
def _infer_cctv_media_type_from_url(target_url: str, content_type: str) -> str:
from urllib.parse import urlparse
clean_type = str(content_type or "").split(";", 1)[0].strip().lower()
if clean_type and clean_type not in {"application/octet-stream", "binary/octet-stream"}:
return content_type
path = str(urlparse(target_url).path or "").lower()
if path.endswith((".jpg", ".jpeg")):
return "image/jpeg"
if path.endswith(".png"):
return "image/png"
if path.endswith(".webp"):
return "image/webp"
if path.endswith(".gif"):
return "image/gif"
if path.endswith(".mp4"):
return "video/mp4"
if path.endswith((".m3u8", ".m3u")):
return "application/vnd.apple.mpegurl"
if path.endswith((".mjpg", ".mjpeg")):
return "multipart/x-mixed-replace"
return content_type or "application/octet-stream"
def _proxy_cctv_media_response(request: Request, target_url: str): def _proxy_cctv_media_response(request: Request, target_url: str):
from urllib.parse import urlparse from urllib.parse import urlparse
from fastapi.responses import Response from fastapi.responses import Response
parsed = urlparse(target_url) parsed = urlparse(target_url)
profile = _cctv_proxy_profile_for_url(target_url) profile = _cctv_proxy_profile_for_url(target_url)
resp = _fetch_cctv_upstream_response(request, target_url, profile) resp = _fetch_cctv_upstream_response(request, target_url, profile)
content_type = resp.headers.get("Content-Type", "application/octet-stream") content_type = _infer_cctv_media_type_from_url(
target_url,
resp.headers.get("Content-Type", "application/octet-stream"),
)
is_hls_playlist = ( is_hls_playlist = (
".m3u8" in str(parsed.path or "").lower() ".m3u8" in str(parsed.path or "").lower()
or "mpegurl" in content_type.lower() or "mpegurl" in content_type.lower()
+13 -2
View File
@@ -324,8 +324,19 @@ async def update_layers(update: LayerUpdate, request: Request):
sigint_grid.mesh.stop() sigint_grid.mesh.stop()
logger.info("Meshtastic MQTT bridge stopped (layer disabled)") logger.info("Meshtastic MQTT bridge stopped (layer disabled)")
elif not old_mesh and new_mesh: elif not old_mesh and new_mesh:
sigint_grid.mesh.start() try:
logger.info("Meshtastic MQTT bridge started (layer enabled)") from services.config import get_settings
mqtt_enabled = bool(getattr(get_settings(), "MESH_MQTT_ENABLED", False))
except Exception:
mqtt_enabled = False
if mqtt_enabled:
sigint_grid.mesh.start()
logger.info("Meshtastic MQTT bridge started (layer enabled)")
else:
logger.info(
"Meshtastic layer enabled; MQTT bridge remains disabled "
"(set MESH_MQTT_ENABLED=true to participate in the public broker)"
)
if old_aprs and not new_aprs: if old_aprs and not new_aprs:
sigint_grid.aprs.stop() sigint_grid.aprs.stop()
logger.info("APRS bridge stopped (layer disabled)") logger.info("APRS bridge stopped (layer disabled)")
+114 -1
View File
@@ -19,6 +19,7 @@ import concurrent.futures
import json import json
import math import math
import os import os
import threading
import time import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
@@ -134,6 +135,10 @@ _INTEL_STARTUP_CACHE_KEYS = (
"correlations", "correlations",
"fimi", "fimi",
) )
_STARTUP_PRIORITY_TIMEOUT_S = float(os.environ.get("SHADOWBROKER_STARTUP_PRIORITY_TIMEOUT_S", "18"))
_STARTUP_HEAVY_REFRESH_DELAY_S = float(os.environ.get("SHADOWBROKER_STARTUP_HEAVY_REFRESH_DELAY_S", "90"))
_STARTUP_HEAVY_REFRESH_STARTED = False
_STARTUP_HEAVY_REFRESH_LOCK = threading.Lock()
# Shared thread pool — reused across all fetch cycles instead of creating/destroying per tick # Shared thread pool — reused across all fetch cycles instead of creating/destroying per tick
_SHARED_EXECUTOR = concurrent.futures.ThreadPoolExecutor( _SHARED_EXECUTOR = concurrent.futures.ThreadPoolExecutor(
@@ -399,6 +404,72 @@ def update_slow_data():
logger.info("Slow-tier update complete.") logger.info("Slow-tier update complete.")
def _record_fetch_success(label: str, name: str, start: float) -> None:
duration = time.perf_counter() - start
from services.fetch_health import record_success
record_success(name, duration_s=duration)
if duration > _SLOW_FETCH_S:
logger.warning(f"{label} task slow: {name} took {duration:.2f}s")
def _record_fetch_failure(label: str, name: str, start: float, error: Exception) -> None:
duration = time.perf_counter() - start
from services.fetch_health import record_failure
record_failure(name, error=error, duration_s=duration)
logger.exception(f"{label} task failed: {name}")
def _load_cctv_cache_for_startup() -> None:
"""Load cached CCTV rows without running remote ingestors during first paint."""
try:
fetch_cctv()
except Exception as e:
logger.warning("Startup CCTV cache load failed (non-fatal): %s", e)
def _run_delayed_startup_heavy_refresh() -> None:
if _STARTUP_HEAVY_REFRESH_DELAY_S > 0:
logger.info(
"Startup heavy synthesis delayed %.0fs so the dashboard can finish first paint",
_STARTUP_HEAVY_REFRESH_DELAY_S,
)
time.sleep(_STARTUP_HEAVY_REFRESH_DELAY_S)
logger.info("Startup heavy synthesis beginning (slow feeds, enrichment, daily products)...")
_run_tasks(
"startup-heavy",
[
update_slow_data,
fetch_volcanoes,
fetch_viirs_change_nodes,
fetch_unusual_whales,
fetch_fimi,
fetch_uap_sightings,
fetch_wastewater,
fetch_sar_catalog,
fetch_sar_products,
],
)
logger.info("Startup heavy synthesis complete.")
def _schedule_delayed_startup_heavy_refresh() -> None:
global _STARTUP_HEAVY_REFRESH_STARTED
if _STARTUP_HEAVY_REFRESH_DELAY_S < 0:
logger.info("Startup heavy synthesis disabled by SHADOWBROKER_STARTUP_HEAVY_REFRESH_DELAY_S")
return
with _STARTUP_HEAVY_REFRESH_LOCK:
if _STARTUP_HEAVY_REFRESH_STARTED:
return
_STARTUP_HEAVY_REFRESH_STARTED = True
threading.Thread(
target=_run_delayed_startup_heavy_refresh,
name="startup-heavy-refresh",
daemon=True,
).start()
def update_all_data(*, startup_mode: bool = False): def update_all_data(*, startup_mode: bool = False):
"""Full refresh. """Full refresh.
@@ -410,6 +481,48 @@ def update_all_data(*, startup_mode: bool = False):
seed_startup_caches() seed_startup_caches()
with _data_lock: with _data_lock:
meshtastic_seeded = bool(latest_data.get("meshtastic_map_nodes")) meshtastic_seeded = bool(latest_data.get("meshtastic_map_nodes"))
if startup_mode:
_load_cctv_cache_for_startup()
priority_funcs = [
fetch_airports,
update_fast_data,
fetch_gdelt,
fetch_crowdthreat,
fetch_firms_fires,
fetch_weather_alerts,
]
if not meshtastic_seeded:
priority_funcs.append(fetch_meshtastic_nodes)
else:
logger.info(
"Startup preload: Meshtastic cache already loaded, deferring remote map refresh to scheduled cadence"
)
logger.info("Startup priority preload starting (%d tasks)...", len(priority_funcs))
cycle_start = time.perf_counter()
futures = {
_SHARED_EXECUTOR.submit(func): (func.__name__, time.perf_counter())
for func in priority_funcs
}
for future, (name, start) in futures.items():
remaining = _STARTUP_PRIORITY_TIMEOUT_S - (time.perf_counter() - cycle_start)
if remaining <= 0:
logger.info("Startup priority budget reached; %s will continue in background", name)
continue
try:
future.result(timeout=remaining)
_record_fetch_success("startup-priority", name, start)
except concurrent.futures.TimeoutError:
logger.info(
"Startup priority task still warming after %.1fs: %s",
time.perf_counter() - start,
name,
)
except Exception as e:
_record_fetch_failure("startup-priority", name, start, e)
logger.info("Startup preload: deferring Playwright Liveuamap scraper to scheduled cadence")
_schedule_delayed_startup_heavy_refresh()
logger.info("Startup priority preload complete; slow synthesis is warming in background.")
return
futures = { futures = {
_SHARED_EXECUTOR.submit(fetch_airports): ("fetch_airports", time.perf_counter()), _SHARED_EXECUTOR.submit(fetch_airports): ("fetch_airports", time.perf_counter()),
_SHARED_EXECUTOR.submit(update_fast_data): ("update_fast_data", time.perf_counter()), _SHARED_EXECUTOR.submit(update_fast_data): ("update_fast_data", time.perf_counter()),
@@ -496,7 +609,7 @@ def update_all_data(*, startup_mode: bool = False):
_scheduler = None _scheduler = None
_STARTUP_CCTV_INGEST_DELAY_S = 30 _STARTUP_CCTV_INGEST_DELAY_S = int(os.environ.get("SHADOWBROKER_STARTUP_CCTV_INGEST_DELAY_S", "180"))
_FINANCIAL_REFRESH_MINUTES = 30 _FINANCIAL_REFRESH_MINUTES = 30
+26 -16
View File
@@ -100,7 +100,14 @@ class TestRequireLocalOperator:
# _validate_peer_push_secret — startup enforcement # _validate_peer_push_secret — startup enforcement
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
_KNOWN_COMPROMISED = "Mv63UvLfwqOEVWeRBXjA8MtFl2nEkkhUlLYVHiX1Zzo" _KNOWN_COMPROMISED = "".join(
[
"Mv63UvLfwq",
"OEVWeRBXjA",
"8MtFl2nEkk",
"hUlLYVHiX1Zzo",
]
)
class TestValidatePeerPushSecret: class TestValidatePeerPushSecret:
@@ -114,16 +121,17 @@ class TestValidatePeerPushSecret:
with patch("main.get_settings", return_value=mock_settings): with patch("main.get_settings", return_value=mock_settings):
return _validate_peer_push_secret return _validate_peer_push_secret
def test_known_default_causes_exit(self): def test_known_default_auto_generates_replacement(self):
from auth import _validate_peer_push_secret from auth import _validate_peer_push_secret
mock_settings = MagicMock() mock_settings = MagicMock()
mock_settings.MESH_PEER_PUSH_SECRET = _KNOWN_COMPROMISED mock_settings.MESH_PEER_PUSH_SECRET = _KNOWN_COMPROMISED
with patch("auth.get_settings", return_value=mock_settings): with (
with pytest.raises(SystemExit) as exc_info: patch("auth.get_settings", return_value=mock_settings),
_validate_peer_push_secret() patch("auth._auto_generate_peer_push_secret", return_value="replacement-secret-value"),
assert exc_info.value.code == 1 ):
_validate_peer_push_secret()
def test_empty_secret_does_not_exit_without_peers(self): def test_empty_secret_does_not_exit_without_peers(self):
from auth import _validate_peer_push_secret from auth import _validate_peer_push_secret
@@ -137,7 +145,7 @@ class TestValidatePeerPushSecret:
with patch("auth.get_settings", return_value=mock_settings): with patch("auth.get_settings", return_value=mock_settings):
_validate_peer_push_secret() # no exception = pass _validate_peer_push_secret() # no exception = pass
def test_empty_secret_with_peers_causes_exit(self): def test_empty_secret_with_peers_auto_generates_replacement(self):
from auth import _validate_peer_push_secret from auth import _validate_peer_push_secret
mock_settings = MagicMock() mock_settings = MagicMock()
@@ -146,12 +154,13 @@ class TestValidatePeerPushSecret:
mock_settings.MESH_RNS_PEERS = "" mock_settings.MESH_RNS_PEERS = ""
mock_settings.MESH_RNS_ENABLED = False mock_settings.MESH_RNS_ENABLED = False
with patch("auth.get_settings", return_value=mock_settings): with (
with pytest.raises(SystemExit) as exc_info: patch("auth.get_settings", return_value=mock_settings),
_validate_peer_push_secret() patch("auth._auto_generate_peer_push_secret", return_value="replacement-secret-value"),
assert exc_info.value.code == 1 ):
_validate_peer_push_secret()
def test_short_secret_with_peers_causes_exit(self): def test_short_secret_with_peers_auto_generates_replacement(self):
from auth import _validate_peer_push_secret from auth import _validate_peer_push_secret
mock_settings = MagicMock() mock_settings = MagicMock()
@@ -160,10 +169,11 @@ class TestValidatePeerPushSecret:
mock_settings.MESH_RNS_PEERS = "" mock_settings.MESH_RNS_PEERS = ""
mock_settings.MESH_RNS_ENABLED = False mock_settings.MESH_RNS_ENABLED = False
with patch("auth.get_settings", return_value=mock_settings): with (
with pytest.raises(SystemExit) as exc_info: patch("auth.get_settings", return_value=mock_settings),
_validate_peer_push_secret() patch("auth._auto_generate_peer_push_secret", return_value="replacement-secret-value"),
assert exc_info.value.code == 1 ):
_validate_peer_push_secret()
def test_valid_secret_passes(self): def test_valid_secret_passes(self):
from auth import _validate_peer_push_secret from auth import _validate_peer_push_secret
@@ -21,14 +21,21 @@ const stagedReleaseAttestationPath = path.join(
const excludedNames = new Set([ const excludedNames = new Set([
'.env', '.env',
'.pytest_cache', '.pytest_cache',
'.ruff_cache',
'__pycache__', '__pycache__',
'backend.egg-info', 'backend.egg-info',
'build', 'build',
'data', 'data',
'tests', 'tests',
'timemachine',
]); ]);
const excludedFiles = new Set([ const excludedFiles = new Set([
'.env.example',
'ais_cache.json',
'carrier_cache.json',
'cctv.db',
'dm_token_pepper.key',
'pytest.ini', 'pytest.ini',
]); ]);
+8 -1
View File
@@ -26,6 +26,7 @@ import GlobalTicker from '@/components/GlobalTicker';
import ErrorBoundary from '@/components/ErrorBoundary'; import ErrorBoundary from '@/components/ErrorBoundary';
import OnboardingModal, { useOnboarding } from '@/components/OnboardingModal'; import OnboardingModal, { useOnboarding } from '@/components/OnboardingModal';
import ChangelogModal, { useChangelog } from '@/components/ChangelogModal'; import ChangelogModal, { useChangelog } from '@/components/ChangelogModal';
import StartupWarmupModal, { useStartupWarmupNotice } from '@/components/StartupWarmupModal';
import type { ActiveLayers, KiwiSDR, Scanner, SelectedEntity } from '@/types/dashboard'; import type { ActiveLayers, KiwiSDR, Scanner, SelectedEntity } from '@/types/dashboard';
import type { ShodanSearchMatch } from '@/types/shodan'; import type { ShodanSearchMatch } from '@/types/shodan';
import { API_BASE } from '@/lib/api'; import { API_BASE } from '@/lib/api';
@@ -452,6 +453,7 @@ export default function Dashboard() {
// Onboarding & connection status // Onboarding & connection status
const { showOnboarding, setShowOnboarding } = useOnboarding(); const { showOnboarding, setShowOnboarding } = useOnboarding();
const { showWarmupNotice, setShowWarmupNotice } = useStartupWarmupNotice();
const { showChangelog, setShowChangelog } = useChangelog(); const { showChangelog, setShowChangelog } = useChangelog();
return ( return (
@@ -930,8 +932,13 @@ export default function Dashboard() {
/> />
)} )}
{/* FIRST-RUN WARMUP NOTICE — shows once after onboarding */}
{!showOnboarding && showWarmupNotice && (
<StartupWarmupModal onClose={() => setShowWarmupNotice(false)} />
)}
{/* v0.4 CHANGELOG MODAL — shows once per version after onboarding */} {/* v0.4 CHANGELOG MODAL — shows once per version after onboarding */}
{!showOnboarding && showChangelog && ( {!showOnboarding && !showWarmupNotice && showChangelog && (
<ChangelogModal onClose={() => setShowChangelog(false)} /> <ChangelogModal onClose={() => setShowChangelog(false)} />
)} )}
+10 -3
View File
@@ -8,8 +8,11 @@ export interface HlsVideoHandle {
get paused(): boolean; get paused(): boolean;
} }
const HlsVideo = forwardRef<HlsVideoHandle, { url: string; className?: string; onError?: () => void }>( const HlsVideo = forwardRef<
({ url, className, onError }, ref) => { HlsVideoHandle,
{ url: string; className?: string; onError?: () => void; onLoaded?: () => void }
>(
({ url, className, onError, onLoaded }, ref) => {
const videoRef = useRef<HTMLVideoElement>(null); const videoRef = useRef<HTMLVideoElement>(null);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
@@ -35,6 +38,7 @@ const HlsVideo = forwardRef<HlsVideoHandle, { url: string; className?: string; o
hls.on(Hls.Events.ERROR, (_e: unknown, data: { fatal?: boolean }) => { hls.on(Hls.Events.ERROR, (_e: unknown, data: { fatal?: boolean }) => {
if (data.fatal) onError?.(); if (data.fatal) onError?.();
}); });
hls.on(Hls.Events.MANIFEST_PARSED, () => onLoaded?.());
hls.loadSource(url); hls.loadSource(url);
hls.attachMedia(video); hls.attachMedia(video);
hlsInstance = hls; hlsInstance = hls;
@@ -47,7 +51,7 @@ const HlsVideo = forwardRef<HlsVideoHandle, { url: string; className?: string; o
cancelled = true; cancelled = true;
hlsInstance?.destroy(); hlsInstance?.destroy();
}; };
}, [url, onError]); }, [url, onError, onLoaded]);
return ( return (
<video <video
@@ -56,6 +60,9 @@ const HlsVideo = forwardRef<HlsVideoHandle, { url: string; className?: string; o
muted muted
playsInline playsInline
onError={() => onError?.()} onError={() => onError?.()}
onCanPlay={() => onLoaded?.()}
onLoadedData={() => onLoaded?.()}
onPlaying={() => onLoaded?.()}
className={className} className={className}
/> />
); );
@@ -5962,6 +5962,7 @@ const MaplibreViewer = ({
return ( return (
<CctvFullscreenModal <CctvFullscreenModal
url={url} url={url}
rawUrl={rawUrl}
mediaType={mt} mediaType={mt}
isVideo={isVideo} isVideo={isVideo}
cameraName={cameraName} cameraName={cameraName}
@@ -1,11 +1,12 @@
'use client'; 'use client';
import React, { useState, useCallback, useRef } from 'react'; import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
import { AlertTriangle, Play, Pause } from 'lucide-react'; import { AlertTriangle, Play, Pause } from 'lucide-react';
import HlsVideo, { type HlsVideoHandle } from '@/components/HlsVideo'; import HlsVideo, { type HlsVideoHandle } from '@/components/HlsVideo';
export interface CctvFullscreenModalProps { export interface CctvFullscreenModalProps {
url: string; url: string;
rawUrl?: string;
mediaType: string; mediaType: string;
isVideo: boolean; isVideo: boolean;
cameraName: string; cameraName: string;
@@ -16,6 +17,7 @@ export interface CctvFullscreenModalProps {
export function CctvFullscreenModal({ export function CctvFullscreenModal({
url, url,
rawUrl = '',
mediaType, mediaType,
isVideo, isVideo,
cameraName, cameraName,
@@ -25,8 +27,60 @@ export function CctvFullscreenModal({
}: CctvFullscreenModalProps) { }: CctvFullscreenModalProps) {
const [paused, setPaused] = useState(false); const [paused, setPaused] = useState(false);
const [mediaError, setMediaError] = useState(false); const [mediaError, setMediaError] = useState(false);
const [mediaLoaded, setMediaLoaded] = useState(false);
const [sourceIndex, setSourceIndex] = useState(0);
const videoRef = useRef<HTMLVideoElement>(null); const videoRef = useRef<HTMLVideoElement>(null);
const hlsRef = useRef<HlsVideoHandle>(null); const hlsRef = useRef<HlsVideoHandle>(null);
const sources = useMemo(() => {
const seen = new Set<string>();
return [url, rawUrl]
.map((candidate) => String(candidate || '').trim())
.filter((candidate) => {
if (!candidate || seen.has(candidate)) return false;
seen.add(candidate);
return true;
});
}, [rawUrl, url]);
const activeUrl = sources[sourceIndex] || '';
useEffect(() => {
setSourceIndex(0);
setMediaError(false);
setMediaLoaded(false);
setPaused(false);
}, [rawUrl, url]);
useEffect(() => {
setMediaLoaded(false);
}, [activeUrl]);
const handleMediaFailure = useCallback(() => {
setSourceIndex((idx) => {
const next = idx + 1;
if (next < sources.length) {
setMediaError(false);
return next;
}
setMediaError(true);
return idx;
});
}, [sources.length]);
const handleMediaReady = useCallback(() => {
setMediaLoaded(true);
}, []);
useEffect(() => {
if (sourceIndex !== 0 || sources.length < 2 || mediaLoaded || mediaError) return;
const timeoutMs = mediaType === 'hls' ? 3200 : 1800;
const timer = window.setTimeout(() => {
setSourceIndex((idx) => {
if (idx !== 0 || mediaLoaded) return idx;
return 1;
});
}, timeoutMs);
return () => window.clearTimeout(timer);
}, [mediaError, mediaLoaded, mediaType, sourceIndex, sources.length]);
const togglePlay = useCallback(() => { const togglePlay = useCallback(() => {
if (mediaType === 'hls') { if (mediaType === 'hls') {
@@ -176,17 +230,21 @@ export function CctvFullscreenModal({
overflow: 'hidden', overflow: 'hidden',
}} }}
> >
{url ? ( {activeUrl ? (
<> <>
{mediaType === 'video' && !mediaError && ( {mediaType === 'video' && !mediaError && (
<video <video
key={activeUrl}
ref={videoRef} ref={videoRef}
src={url} src={activeUrl}
autoPlay autoPlay
loop loop
muted muted
playsInline playsInline
onError={() => setMediaError(true)} onError={handleMediaFailure}
onCanPlay={handleMediaReady}
onLoadedData={handleMediaReady}
onPlaying={handleMediaReady}
style={{ style={{
maxWidth: '100%', maxWidth: '100%',
maxHeight: 'calc(100vh - 260px)', maxHeight: 'calc(100vh - 260px)',
@@ -197,15 +255,18 @@ export function CctvFullscreenModal({
)} )}
{mediaType === 'hls' && !mediaError && ( {mediaType === 'hls' && !mediaError && (
<HlsVideo <HlsVideo
key={activeUrl}
ref={hlsRef} ref={hlsRef}
url={url} url={activeUrl}
onError={() => setMediaError(true)} onError={handleMediaFailure}
className="" onLoaded={handleMediaReady}
className="max-w-full max-h-[calc(100vh-260px)] object-contain"
/> />
)} )}
{mediaType === 'mjpeg' && ( {mediaType === 'mjpeg' && (
<img <img
src={url} key={activeUrl}
src={activeUrl}
alt="MJPEG Feed" alt="MJPEG Feed"
style={{ style={{
maxWidth: '100%', maxWidth: '100%',
@@ -213,14 +274,14 @@ export function CctvFullscreenModal({
objectFit: 'contain', objectFit: 'contain',
filter: 'contrast(1.25) saturate(0.5)', filter: 'contrast(1.25) saturate(0.5)',
}} }}
onError={(e) => { onError={handleMediaFailure}
(e.target as HTMLImageElement).style.display = 'none'; onLoad={handleMediaReady}
}}
/> />
)} )}
{(mediaType === 'image' || mediaType === 'satellite') && ( {(mediaType === 'image' || mediaType === 'satellite') && (
<img <img
src={url} key={activeUrl}
src={activeUrl}
alt="CCTV Feed" alt="CCTV Feed"
style={{ style={{
maxWidth: '100%', maxWidth: '100%',
@@ -228,10 +289,8 @@ export function CctvFullscreenModal({
objectFit: 'contain', objectFit: 'contain',
filter: 'contrast(1.25) saturate(0.5)', filter: 'contrast(1.25) saturate(0.5)',
}} }}
onError={(e) => { onError={handleMediaFailure}
const target = e.target as HTMLImageElement; onLoad={handleMediaReady}
target.style.display = 'none';
}}
/> />
)} )}
@@ -239,7 +298,7 @@ export function CctvFullscreenModal({
{mediaError && ( {mediaError && (
<div style={{ fontSize: 11, color: 'rgba(239,68,68,0.7)', fontFamily: 'monospace', letterSpacing: '0.15em', textAlign: 'center', padding: 40 }}> <div style={{ fontSize: 11, color: 'rgba(239,68,68,0.7)', fontFamily: 'monospace', letterSpacing: '0.15em', textAlign: 'center', padding: 40 }}>
FEED UNAVAILABLE<br /> FEED UNAVAILABLE<br />
<span style={{ fontSize: 9, color: 'rgba(148,163,184,0.5)' }}>stream failed to load — source may be offline</span> <span style={{ fontSize: 9, color: 'rgba(148,163,184,0.5)' }}>proxy and direct source both failed</span>
</div> </div>
)} )}
@@ -329,10 +388,10 @@ export function CctvFullscreenModal({
{cameraName} {cameraName}
</span> </span>
<div style={{ display: 'flex', gap: 10 }}> <div style={{ display: 'flex', gap: 10 }}>
{url && ( {activeUrl && (
<> <>
<a <a
href={url} href={rawUrl || activeUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style={{ style={{
@@ -354,7 +413,7 @@ export function CctvFullscreenModal({
<button <button
onClick={async () => { onClick={async () => {
try { try {
await navigator.clipboard.writeText(url); await navigator.clipboard.writeText(rawUrl || activeUrl);
} catch { /* ignore */ } } catch { /* ignore */ }
}} }}
style={{ style={{
+66 -27
View File
@@ -111,6 +111,7 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
identityWizardStatus, identityWizardStatus,
setIdentityWizardStatus, setIdentityWizardStatus,
meshQuickStatus, meshQuickStatus,
meshSessionActive,
publicMeshAddress, publicMeshAddress,
meshView, meshView,
setMeshView, setMeshView,
@@ -119,6 +120,7 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
// Identity // Identity
identity, identity,
publicIdentity, publicIdentity,
hasStoredPublicLaneIdentity,
hasPublicLaneIdentity, hasPublicLaneIdentity,
hasId, hasId,
shouldShowIdentityWarning, shouldShowIdentityWarning,
@@ -255,6 +257,7 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
openChat, openChat,
handleCreatePublicIdentity, handleCreatePublicIdentity,
handleQuickCreatePublicIdentity, handleQuickCreatePublicIdentity,
handleActivatePublicMeshSession,
handleLeaveWormholeForPublicMesh, handleLeaveWormholeForPublicMesh,
handleResetPublicIdentity, handleResetPublicIdentity,
handleBootstrapPrivateIdentity, handleBootstrapPrivateIdentity,
@@ -324,6 +327,40 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
} }
void handleRequestAccess(targetId); void handleRequestAccess(targetId);
}; };
const meshActivationText =
meshQuickStatus?.text ||
(publicMeshBlockedByWormhole
? hasStoredPublicLaneIdentity
? 'Wormhole is active. Turning MeshChat on will turn Wormhole off and use your saved public mesh key.'
: 'Wormhole is active. Turning MeshChat on will turn Wormhole off and mint a separate public mesh key.'
: hasStoredPublicLaneIdentity
? 'MeshChat is off. Turn it on to use your saved public mesh key.'
: 'Public mesh posting needs a mesh key. One tap gets you a fresh address.');
const handleMeshActivationAction = () => {
if (hasStoredPublicLaneIdentity) {
void handleActivatePublicMeshSession();
return;
}
if (publicMeshBlockedByWormhole) {
void handleLeaveWormholeForPublicMesh();
return;
}
void handleQuickCreatePublicIdentity();
};
const meshActivationLabel = identityWizardBusy
? 'GETTING MESH KEY'
: hasStoredPublicLaneIdentity
? 'TURN ON MESH'
: publicMeshBlockedByWormhole
? 'TURN OFF WORMHOLE FOR MESH'
: 'GET MESH KEY';
const meshActivationSideLabel = identityWizardBusy
? 'WORKING...'
: hasStoredPublicLaneIdentity
? 'USE SAVED KEY'
: publicMeshBlockedByWormhole
? 'AUTO DISABLE'
: 'ONE TAP';
return ( return (
<div <div
@@ -1120,16 +1157,25 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
</button> </button>
</div> </div>
<div className="text-[10px] font-mono text-[var(--text-muted)] truncate"> <div className="text-[10px] font-mono text-[var(--text-muted)] truncate">
{publicMeshAddress ? `ADDR ${publicMeshAddress.toUpperCase()}` : 'NO PUBLIC MESH ADDRESS'} {meshSessionActive && publicMeshAddress
? `ADDR ${publicMeshAddress.toUpperCase()}`
: publicMeshAddress
? 'MESH OFF / KEY SAVED'
: 'NO PUBLIC MESH ADDRESS'}
</div> </div>
</div> </div>
<div className="flex-1 overflow-y-auto styled-scrollbar px-3 py-1.5 border-l-2 border-cyan-800/25"> <div className="flex-1 overflow-y-auto styled-scrollbar px-3 py-1.5 border-l-2 border-cyan-800/25">
{meshView === 'channel' && filteredMeshMessages.length === 0 && ( {!meshSessionActive && (
<div className="text-[12px] font-mono text-green-300/70 text-center py-4 leading-[1.65]">
MeshChat is off. Turn it on to connect the public mesh lane.
</div>
)}
{meshSessionActive && meshView === 'channel' && filteredMeshMessages.length === 0 && (
<div className="text-[12px] font-mono text-[var(--text-muted)] text-center py-4 leading-[1.65]"> <div className="text-[12px] font-mono text-[var(--text-muted)] text-center py-4 leading-[1.65]">
No messages from {meshRegion} / {meshChannel} No messages from {meshRegion} / {meshChannel}
</div> </div>
)} )}
{meshView === 'inbox' && ( {meshSessionActive && meshView === 'inbox' && (
<> <>
{!publicMeshAddress && ( {!publicMeshAddress && (
<div className="text-[12px] font-mono text-[var(--text-muted)] text-center py-4 leading-[1.65]"> <div className="text-[12px] font-mono text-[var(--text-muted)] text-center py-4 leading-[1.65]">
@@ -2049,7 +2095,9 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
? meshDirectTarget ? meshDirectTarget
? `→ MESH / TO ${meshDirectTarget.toUpperCase()}` ? `→ MESH / TO ${meshDirectTarget.toUpperCase()}`
: `→ MESH / ${meshRegion} / ${meshChannel}` : `→ MESH / ${meshRegion} / ${meshChannel}`
: '→ MESH LOCKED' : hasStoredPublicLaneIdentity
? '→ MESH OFF'
: '→ MESH LOCKED'
: activeTab === 'dms' && secureDmBlocked : activeTab === 'dms' && secureDmBlocked
? '→ DEAD DROP LOCKED' ? '→ DEAD DROP LOCKED'
: dmView === 'chat' && selectedContact : dmView === 'chat' && selectedContact
@@ -2068,10 +2116,7 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
: 'text-green-300/70' : 'text-green-300/70'
}`} }`}
> >
{meshQuickStatus?.text || {meshActivationText}
(publicMeshBlockedByWormhole
? 'Wormhole is active. Turn it off here and we will mint a separate public mesh key for you.'
: 'Public mesh posting needs a mesh key. One tap gets you a fresh address.')}
</div> </div>
)} )}
<div className="flex items-center gap-2 px-3 pb-2 pt-1"> <div className="flex items-center gap-2 px-3 pb-2 pt-1">
@@ -2103,30 +2148,16 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
</button> </button>
) : activeTab === 'meshtastic' && !hasPublicLaneIdentity ? ( ) : activeTab === 'meshtastic' && !hasPublicLaneIdentity ? (
<button <button
onClick={() => { onClick={handleMeshActivationAction}
if (publicMeshBlockedByWormhole) {
void handleLeaveWormholeForPublicMesh();
return;
}
void handleQuickCreatePublicIdentity();
}}
disabled={identityWizardBusy} disabled={identityWizardBusy}
className="w-full flex items-center justify-between gap-2 px-3 py-2 border border-green-700/40 bg-green-950/15 text-green-300 hover:bg-green-950/25 hover:border-green-500/50 transition-colors" className="w-full flex items-center justify-between gap-2 px-3 py-2 border border-green-700/40 bg-green-950/15 text-green-300 hover:bg-green-950/25 hover:border-green-500/50 transition-colors"
> >
<span className="inline-flex items-center gap-2 text-sm font-mono tracking-[0.2em]"> <span className="inline-flex items-center gap-2 text-sm font-mono tracking-[0.2em]">
<Radio size={11} /> <Radio size={11} />
{identityWizardBusy {meshActivationLabel}
? 'GETTING MESH KEY'
: publicMeshBlockedByWormhole
? 'TURN OFF WORMHOLE FOR MESH'
: 'GET MESH KEY'}
</span> </span>
<span className="text-[12px] font-mono text-green-300/70"> <span className="text-[12px] font-mono text-green-300/70">
{identityWizardBusy {meshActivationSideLabel}
? 'WORKING...'
: publicMeshBlockedByWormhole
? 'AUTO FIX'
: 'ONE TAP'}
</span> </span>
</button> </button>
) : activeTab === 'meshtastic' && meshDirectTarget ? ( ) : activeTab === 'meshtastic' && meshDirectTarget ? (
@@ -2375,8 +2406,8 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
CURRENT STATE CURRENT STATE
</div> </div>
<div className="grid grid-cols-1 gap-1 text-[13px] font-mono text-[var(--text-secondary)] leading-[1.5]"> <div className="grid grid-cols-1 gap-1 text-[13px] font-mono text-[var(--text-secondary)] leading-[1.5]">
<div>Public mesh key: {hasPublicLaneIdentity ? 'active' : 'not issued'}</div> <div>Public mesh key: {hasPublicLaneIdentity ? 'active' : hasStoredPublicLaneIdentity ? 'saved / off' : 'not issued'}</div>
<div>Public mesh address: {hasPublicLaneIdentity && publicMeshAddress ? publicMeshAddress.toUpperCase() : 'not ready'}</div> <div>Public mesh address: {publicMeshAddress ? publicMeshAddress.toUpperCase() : 'not ready'}</div>
<div>Wormhole lane: {wormholeEnabled && wormholeReadyState ? 'active' : wormholeEnabled ? 'starting' : 'off'}</div> <div>Wormhole lane: {wormholeEnabled && wormholeReadyState ? 'active' : wormholeEnabled ? 'starting' : 'off'}</div>
<div>Wormhole descriptor: {wormholeDescriptor?.nodeId || 'not cached yet'}</div> <div>Wormhole descriptor: {wormholeDescriptor?.nodeId || 'not cached yet'}</div>
</div> </div>
@@ -2385,6 +2416,10 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
<div className="grid grid-cols-1 gap-2"> <div className="grid grid-cols-1 gap-2">
<button <button
onClick={() => { onClick={() => {
if (hasStoredPublicLaneIdentity) {
void handleActivatePublicMeshSession();
return;
}
if (publicMeshBlockedByWormhole) { if (publicMeshBlockedByWormhole) {
void handleLeaveWormholeForPublicMesh(); void handleLeaveWormholeForPublicMesh();
return; return;
@@ -2396,12 +2431,16 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
> >
{hasPublicLaneIdentity {hasPublicLaneIdentity
? 'MESH KEY ACTIVE' ? 'MESH KEY ACTIVE'
: hasStoredPublicLaneIdentity
? 'TURN ON MESH'
: publicMeshBlockedByWormhole : publicMeshBlockedByWormhole
? 'TURN OFF WORMHOLE FOR MESH' ? 'TURN OFF WORMHOLE FOR MESH'
: 'GET MESH KEY'} : 'GET MESH KEY'}
<div className="mt-1 text-[13px] text-green-200/70 normal-case tracking-normal leading-[1.45]"> <div className="mt-1 text-[13px] text-green-200/70 normal-case tracking-normal leading-[1.45]">
{hasPublicLaneIdentity {hasPublicLaneIdentity
? 'Your public mesh key is already live for posting.' ? 'Your public mesh key is already live for posting.'
: hasStoredPublicLaneIdentity
? 'Use your saved public mesh key. This turns Wormhole off first if it is active.'
: publicMeshBlockedByWormhole : publicMeshBlockedByWormhole
? 'One tap turns Wormhole off and mints a separate public mesh key.' ? 'One tap turns Wormhole off and mints a separate public mesh key.'
: 'One tap for a working mesh key and address.'} : 'One tap for a working mesh key and address.'}
@@ -313,6 +313,7 @@ export function useMeshChatController({
const [identityWizardBusy, setIdentityWizardBusy] = useState(false); const [identityWizardBusy, setIdentityWizardBusy] = useState(false);
const [identityWizardStatus, setIdentityWizardStatus] = useState<{ type: 'ok' | 'err'; text: string } | null>(null); const [identityWizardStatus, setIdentityWizardStatus] = useState<{ type: 'ok' | 'err'; text: string } | null>(null);
const [meshQuickStatus, setMeshQuickStatus] = useState<{ type: 'ok' | 'err'; text: string } | null>(null); const [meshQuickStatus, setMeshQuickStatus] = useState<{ type: 'ok' | 'err'; text: string } | null>(null);
const [meshSessionActive, setMeshSessionActive] = useState(false);
const [publicMeshAddress, setPublicMeshAddress] = useState(''); const [publicMeshAddress, setPublicMeshAddress] = useState('');
const [meshView, setMeshView] = useState<'channel' | 'inbox'>('channel'); const [meshView, setMeshView] = useState<'channel' | 'inbox'>('channel');
const [meshDirectTarget, setMeshDirectTarget] = useState(''); const [meshDirectTarget, setMeshDirectTarget] = useState('');
@@ -328,12 +329,14 @@ export function useMeshChatController({
const [recentPrivateFallbackReason, setRecentPrivateFallbackReason] = useState(''); const [recentPrivateFallbackReason, setRecentPrivateFallbackReason] = useState('');
const [unresolvedSenderSealCount, setUnresolvedSenderSealCount] = useState(0); const [unresolvedSenderSealCount, setUnresolvedSenderSealCount] = useState(0);
const [privacyProfile, setPrivacyProfile] = useState<'default' | 'high'>('default'); const [privacyProfile, setPrivacyProfile] = useState<'default' | 'high'>('default');
const publicIdentity = clientHydrated ? getNodeIdentity() : null; const storedPublicIdentity = clientHydrated ? getNodeIdentity() : null;
const hasPublicLaneIdentity = clientHydrated && Boolean(publicIdentity) && hasSovereignty(); const hasStoredPublicLaneIdentity = clientHydrated && Boolean(storedPublicIdentity) && hasSovereignty();
const publicIdentity = meshSessionActive ? storedPublicIdentity : null;
const hasPublicLaneIdentity = meshSessionActive && hasStoredPublicLaneIdentity;
const hasId = Boolean(identity) && (hasSovereignty() || wormholeEnabled); const hasId = Boolean(identity) && (hasSovereignty() || wormholeEnabled);
const shouldShowIdentityWarning = activeTab !== 'meshtastic' && !hasId; const shouldShowIdentityWarning = activeTab !== 'meshtastic' && !hasId;
const privateInfonetReady = wormholeEnabled && wormholeReadyState; const privateInfonetReady = wormholeEnabled && wormholeReadyState;
const publicMeshBlockedByWormhole = wormholeEnabled && wormholeReadyState && !hasPublicLaneIdentity; const publicMeshBlockedByWormhole = wormholeEnabled || wormholeReadyState;
const dmSendQueue = useRef<(() => Promise<void>)[]>([]); const dmSendQueue = useRef<(() => Promise<void>)[]>([]);
const dmSendTimer = useRef<ReturnType<typeof setTimeout> | null>(null); const dmSendTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const streamEnabledForSelectedGateRef = useRef(false); const streamEnabledForSelectedGateRef = useRef(false);
@@ -365,6 +368,13 @@ export function useMeshChatController({
setClientHydrated(true); setClientHydrated(true);
}, []); }, []);
useEffect(() => {
if (!clientHydrated) return;
setMeshSessionActive(false);
setMeshMessages([]);
setMeshQuickStatus(null);
}, [clientHydrated]);
useEffect( useEffect(
() => () =>
subscribeGateSessionStreamStatus((nextStatus) => { subscribeGateSessionStreamStatus((nextStatus) => {
@@ -450,6 +460,8 @@ export function useMeshChatController({
setSecureModeCached(enabled); setSecureModeCached(enabled);
setWormholeEnabled(enabled); setWormholeEnabled(enabled);
if (enabled) { if (enabled) {
setMeshSessionActive(false);
setMeshMessages([]);
purgeBrowserContactGraph(); purgeBrowserContactGraph();
void hydrateWormholeContacts(); void hydrateWormholeContacts();
} }
@@ -515,7 +527,7 @@ export function useMeshChatController({
useEffect(() => { useEffect(() => {
let alive = true; let alive = true;
const senderId = publicIdentity?.nodeId || ''; const senderId = storedPublicIdentity?.nodeId || '';
if (!senderId || !globalThis.crypto?.subtle) { if (!senderId || !globalThis.crypto?.subtle) {
setPublicMeshAddress(''); setPublicMeshAddress('');
return; return;
@@ -530,7 +542,7 @@ export function useMeshChatController({
return () => { return () => {
alive = false; alive = false;
}; };
}, [publicIdentity?.nodeId]); }, [storedPublicIdentity?.nodeId]);
const flushDmQueue = useCallback(async () => { const flushDmQueue = useCallback(async () => {
const queue = dmSendQueue.current.splice(0); const queue = dmSendQueue.current.splice(0);
@@ -1138,10 +1150,10 @@ export function useMeshChatController({
[meshMessages, mutedUsers], [meshMessages, mutedUsers],
); );
const meshInboxMessages = useMemo(() => { const meshInboxMessages = useMemo(() => {
if (!publicMeshAddress) return []; if (!meshSessionActive || !publicMeshAddress) return [];
const target = publicMeshAddress.toLowerCase(); const target = publicMeshAddress.toLowerCase();
return filteredMeshMessages.filter((m) => String(m.to || '').toLowerCase() === target); return filteredMeshMessages.filter((m) => String(m.to || '').toLowerCase() === target);
}, [filteredMeshMessages, publicMeshAddress]); }, [filteredMeshMessages, meshSessionActive, publicMeshAddress]);
// ─── InfoNet Polling ───────────────────────────────────────────────────── // ─── InfoNet Polling ─────────────────────────────────────────────────────
@@ -1735,7 +1747,7 @@ export function useMeshChatController({
// ─── Meshtastic Channel Discovery ────────────────────────────────────── // ─── Meshtastic Channel Discovery ──────────────────────────────────────
useEffect(() => { useEffect(() => {
if (!expanded || activeTab !== 'meshtastic') return; if (!expanded || activeTab !== 'meshtastic' || !meshSessionActive) return;
let cancelled = false; let cancelled = false;
const fetchChannels = async () => { const fetchChannels = async () => {
try { try {
@@ -1794,12 +1806,12 @@ export function useMeshChatController({
cancelled = true; cancelled = true;
clearInterval(iv); clearInterval(iv);
}; };
}, [expanded, activeTab, meshRegion]); }, [expanded, activeTab, meshRegion, meshSessionActive]);
// ─── Meshtastic Polling ────────────────────────────────────────────────── // ─── Meshtastic Polling ──────────────────────────────────────────────────
useEffect(() => { useEffect(() => {
if (!expanded || activeTab !== 'meshtastic') return; if (!expanded || activeTab !== 'meshtastic' || !meshSessionActive) return;
let cancelled = false; let cancelled = false;
const poll = async () => { const poll = async () => {
try { try {
@@ -1823,7 +1835,13 @@ export function useMeshChatController({
cancelled = true; cancelled = true;
clearInterval(iv); clearInterval(iv);
}; };
}, [expanded, activeTab, meshRegion, meshChannel, meshView]); }, [expanded, activeTab, meshRegion, meshChannel, meshView, meshSessionActive]);
useEffect(() => {
if (meshSessionActive) return;
setMeshMessages([]);
setMeshQuickStatus(null);
}, [meshSessionActive]);
// ─── DM Polling ────────────────────────────────────────────────────────── // ─── DM Polling ──────────────────────────────────────────────────────────
@@ -2305,7 +2323,8 @@ export function useMeshChatController({
const handleSend = async () => { const handleSend = async () => {
const msg = inputValue.trim(); const msg = inputValue.trim();
if (!msg || !hasId || busy) return; if (!msg || busy) return;
if (activeTab !== 'meshtastic' && !hasId) return;
const cooldownMs = activeTab === 'dms' ? 0 : 30_000; const cooldownMs = activeTab === 'dms' ? 0 : 30_000;
const now = Date.now(); const now = Date.now();
@@ -2392,13 +2411,15 @@ export function useMeshChatController({
]); ]);
setGateReplyContext(null); setGateReplyContext(null);
} else if (activeTab === 'meshtastic') { } else if (activeTab === 'meshtastic') {
if (!publicIdentity || !hasSovereignty()) { if (!meshSessionActive || !publicIdentity || !hasSovereignty()) {
setInputValue(msg); setInputValue(msg);
setLastSendTime(0); setLastSendTime(0);
setSendError('public mesh identity needed'); setSendError(meshSessionActive ? 'public mesh identity needed' : 'meshchat is off');
openIdentityWizard({ openIdentityWizard({
type: 'err', type: 'err',
text: 'Quick fix: create a public mesh identity below, then retry your send.', text: hasStoredPublicLaneIdentity
? 'Quick fix: turn MeshChat on below, then retry your send.'
: 'Quick fix: create a public mesh identity below, then retry your send.',
}); });
setTimeout(() => setSendError(''), 4000); setTimeout(() => setSendError(''), 4000);
setBusy(false); setBusy(false);
@@ -3959,16 +3980,36 @@ export function useMeshChatController({
[inputDisabled], [inputDisabled],
); );
const disableWormholeForPublicMesh = useCallback(async () => {
const requireBackendLeave = wormholeEnabled || wormholeReadyState;
try {
await leaveWormhole();
} catch (err) {
if (requireBackendLeave) {
throw err;
}
}
setWormholeEnabled(false);
setWormholeReadyState(false);
setWormholeRnsReady(false);
setWormholeRnsDirectReady(false);
setWormholeRnsPeers({ active: 0, configured: 0 });
setSecureModeCached(false);
}, [wormholeEnabled, wormholeReadyState]);
const createPublicMeshIdentity = useCallback( const createPublicMeshIdentity = useCallback(
async ({ closeWizardOnSuccess }: { closeWizardOnSuccess: boolean }) => { async ({ closeWizardOnSuccess }: { closeWizardOnSuccess: boolean }) => {
setIdentityWizardBusy(true); setIdentityWizardBusy(true);
setIdentityWizardStatus(null); setIdentityWizardStatus(null);
try { try {
await disableWormholeForPublicMesh();
const nextIdentity = await generateNodeKeys(); const nextIdentity = await generateNodeKeys();
const nextAddress = await derivePublicMeshAddress(nextIdentity.nodeId).catch(() => ''); const nextAddress = await derivePublicMeshAddress(nextIdentity.nodeId).catch(() => '');
const readyAddress = (nextAddress || nextIdentity.nodeId).toUpperCase(); const readyAddress = (nextAddress || nextIdentity.nodeId).toUpperCase();
setIdentity(nextIdentity); setIdentity(nextIdentity);
setPublicMeshAddress(nextAddress || nextIdentity.nodeId); setPublicMeshAddress(nextAddress || nextIdentity.nodeId);
setMeshSessionActive(true);
setMeshMessages([]);
setSendError(''); setSendError('');
const successText = `Mesh key ready. Address ${readyAddress} is live for this testnet session.`; const successText = `Mesh key ready. Address ${readyAddress} is live for this testnet session.`;
setIdentityWizardStatus({ setIdentityWizardStatus({
@@ -3997,7 +4038,7 @@ export function useMeshChatController({
setIdentityWizardBusy(false); setIdentityWizardBusy(false);
} }
}, },
[], [disableWormholeForPublicMesh],
); );
const handleCreatePublicIdentity = useCallback(async () => { const handleCreatePublicIdentity = useCallback(async () => {
@@ -4013,6 +4054,45 @@ export function useMeshChatController({
} }
}, [createPublicMeshIdentity]); }, [createPublicMeshIdentity]);
const handleActivatePublicMeshSession = useCallback(async () => {
setIdentityWizardBusy(true);
setIdentityWizardStatus(null);
setMeshQuickStatus(null);
try {
const savedIdentity = getNodeIdentity();
if (!savedIdentity || !hasSovereignty()) {
const text = 'No saved public mesh key is available. Create a mesh key first.';
setMeshSessionActive(false);
setIdentityWizardStatus({ type: 'err', text });
setMeshQuickStatus({ type: 'err', text });
return { ok: false as const, text };
}
await disableWormholeForPublicMesh();
const nextAddress = await derivePublicMeshAddress(savedIdentity.nodeId).catch(() => '');
const readyAddress = (nextAddress || savedIdentity.nodeId).toUpperCase();
setIdentity(savedIdentity);
setPublicMeshAddress(nextAddress || savedIdentity.nodeId);
setMeshSessionActive(true);
setMeshMessages([]);
setSendError('');
const text = `MeshChat is on with saved address ${readyAddress}.`;
setIdentityWizardStatus({ type: 'ok', text });
setMeshQuickStatus({ type: 'ok', text });
return { ok: true as const, text };
} catch (err) {
const message =
typeof err === 'object' && err !== null && 'message' in err
? String((err as { message?: string }).message)
: 'unknown error';
const text = `Could not turn MeshChat on: ${message}`;
setIdentityWizardStatus({ type: 'err', text });
setMeshQuickStatus({ type: 'err', text });
return { ok: false as const, text };
} finally {
setIdentityWizardBusy(false);
}
}, [disableWormholeForPublicMesh]);
const handleReplyToMeshAddress = useCallback((address: string) => { const handleReplyToMeshAddress = useCallback((address: string) => {
const target = String(address || '').trim(); const target = String(address || '').trim();
if (!target) return; if (!target) return;
@@ -4023,36 +4103,16 @@ export function useMeshChatController({
}, []); }, []);
const handleLeaveWormholeForPublicMesh = useCallback(async () => { const handleLeaveWormholeForPublicMesh = useCallback(async () => {
setIdentityWizardBusy(true); const result = hasStoredPublicLaneIdentity
setIdentityWizardStatus(null); ? await handleActivatePublicMeshSession()
setMeshQuickStatus(null); : await createPublicMeshIdentity({ closeWizardOnSuccess: false });
try { const status = { type: result.ok ? 'ok' as const : 'err' as const, text: result.text };
await leaveWormhole(); setIdentityWizardStatus(status);
setWormholeEnabled(false); setMeshQuickStatus(status);
setWormholeReadyState(false); if (result.ok) {
setWormholeRnsReady(false); window.setTimeout(() => setIdentityWizardOpen(false), 900);
setWormholeRnsDirectReady(false);
setWormholeRnsPeers({ active: 0, configured: 0 });
setSecureModeCached(false);
const result = await createPublicMeshIdentity({ closeWizardOnSuccess: false });
const status = { type: result.ok ? 'ok' as const : 'err' as const, text: result.text };
setIdentityWizardStatus(status);
setMeshQuickStatus(status);
if (result.ok) {
window.setTimeout(() => setIdentityWizardOpen(false), 900);
}
} catch (err) {
const message =
typeof err === 'object' && err !== null && 'message' in err
? String((err as { message?: string }).message)
: 'unknown error';
const text = `Could not turn Wormhole off for public mesh: ${message}`;
setIdentityWizardStatus({ type: 'err', text });
setMeshQuickStatus({ type: 'err', text });
} finally {
setIdentityWizardBusy(false);
} }
}, [createPublicMeshIdentity]); }, [createPublicMeshIdentity, handleActivatePublicMeshSession, hasStoredPublicLaneIdentity]);
const handleResetPublicIdentity = useCallback(async () => { const handleResetPublicIdentity = useCallback(async () => {
if (wormholeEnabled && wormholeReadyState) { if (wormholeEnabled && wormholeReadyState) {
@@ -4065,8 +4125,11 @@ export function useMeshChatController({
setIdentityWizardBusy(true); setIdentityWizardBusy(true);
setIdentityWizardStatus(null); setIdentityWizardStatus(null);
try { try {
setMeshSessionActive(false);
setMeshMessages([]);
await clearBrowserIdentityState(); await clearBrowserIdentityState();
setIdentity(null); setIdentity(null);
setPublicMeshAddress('');
setContacts({}); setContacts({});
setSelectedContact(''); setSelectedContact('');
setDmMessages([]); setDmMessages([]);
@@ -4091,6 +4154,8 @@ export function useMeshChatController({
}, [wormholeEnabled, wormholeReadyState]); }, [wormholeEnabled, wormholeReadyState]);
const handleBootstrapPrivateIdentity = useCallback(async () => { const handleBootstrapPrivateIdentity = useCallback(async () => {
setMeshSessionActive(false);
setMeshMessages([]);
if (wormholeEnabled && wormholeReadyState) { if (wormholeEnabled && wormholeReadyState) {
setIdentityWizardStatus({ setIdentityWizardStatus({
type: 'ok', type: 'ok',
@@ -4175,6 +4240,7 @@ export function useMeshChatController({
identityWizardStatus, identityWizardStatus,
setIdentityWizardStatus, setIdentityWizardStatus,
meshQuickStatus, meshQuickStatus,
meshSessionActive,
publicMeshAddress, publicMeshAddress,
meshView, meshView,
setMeshView, setMeshView,
@@ -4183,6 +4249,7 @@ export function useMeshChatController({
// Identity // Identity
identity, identity,
publicIdentity, publicIdentity,
hasStoredPublicLaneIdentity,
hasPublicLaneIdentity, hasPublicLaneIdentity,
hasId, hasId,
shouldShowIdentityWarning, shouldShowIdentityWarning,
@@ -4320,6 +4387,7 @@ export function useMeshChatController({
openChat, openChat,
handleCreatePublicIdentity, handleCreatePublicIdentity,
handleQuickCreatePublicIdentity, handleQuickCreatePublicIdentity,
handleActivatePublicMeshSession,
handleLeaveWormholeForPublicMesh, handleLeaveWormholeForPublicMesh,
handleResetPublicIdentity, handleResetPublicIdentity,
handleBootstrapPrivateIdentity, handleBootstrapPrivateIdentity,
@@ -0,0 +1,106 @@
'use client';
import React, { useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Database, Clock, X } from 'lucide-react';
const CURRENT_VERSION = '0.9.7';
const STORAGE_KEY = `shadowbroker_startup_warmup_notice_v${CURRENT_VERSION}`;
interface StartupWarmupModalProps {
onClose: () => void;
}
export default function StartupWarmupModal({ onClose }: StartupWarmupModalProps) {
const handleDismiss = () => {
localStorage.setItem(STORAGE_KEY, 'true');
onClose();
};
return (
<AnimatePresence>
<motion.div
key="warmup-backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-[10000]"
onClick={handleDismiss}
/>
<motion.div
key="warmup-modal"
initial={{ opacity: 0, scale: 0.92, y: 18 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.92, y: 18 }}
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
className="fixed inset-0 z-[10001] flex items-center justify-center pointer-events-none"
>
<div
className="w-[520px] max-w-[calc(100vw-32px)] bg-[var(--bg-secondary)]/98 border border-cyan-900/50 pointer-events-auto overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
<div className="p-5 border-b border-[var(--border-primary)]/80 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-cyan-500/10 border border-cyan-500/30 flex items-center justify-center">
<Database size={18} className="text-cyan-400" />
</div>
<div>
<h2 className="text-sm font-bold tracking-[0.2em] text-[var(--text-primary)] font-mono">
STARTUP CACHE
</h2>
<span className="text-[13px] text-[var(--text-muted)] font-mono tracking-widest">
FIRST RUN WARMUP
</span>
</div>
</div>
<button
onClick={handleDismiss}
className="w-8 h-8 border border-[var(--border-primary)] hover:border-red-500/50 flex items-center justify-center text-[var(--text-muted)] hover:text-red-400 transition-all hover:bg-red-950/20"
>
<X size={14} />
</button>
</div>
<div className="p-5 space-y-4">
<div className="bg-cyan-950/20 border border-cyan-500/20 p-4">
<div className="flex items-start gap-3">
<Clock size={15} className="text-cyan-400 mt-0.5 flex-shrink-0" />
<div className="space-y-2">
<p className="text-[11px] text-cyan-300 font-mono font-bold tracking-widest">
MASS DATA SYNTHESIS
</p>
<p className="text-sm text-[var(--text-secondary)] font-mono leading-relaxed">
The first launch builds local caches for flights, ships, satellites, CCTV, fires,
and threat intelligence. Cached launches paint the map much faster; a brand-new
install can take a few minutes while upstream feeds are synthesized.
</p>
</div>
</div>
</div>
<button
onClick={handleDismiss}
className="w-full py-3 border border-cyan-500/40 text-cyan-300 hover:text-cyan-100 hover:border-cyan-400/70 hover:bg-cyan-950/30 transition-all font-mono text-[12px] tracking-[0.18em] font-bold"
>
CONTINUE
</button>
</div>
</div>
</motion.div>
</AnimatePresence>
);
}
export function useStartupWarmupNotice() {
const [showWarmupNotice, setShowWarmupNotice] = useState(false);
useEffect(() => {
try {
setShowWarmupNotice(localStorage.getItem(STORAGE_KEY) !== 'true');
} catch {
setShowWarmupNotice(false);
}
}, []);
return { showWarmupNotice, setShowWarmupNotice };
}