feat: Add sensor plugin framework and Sensors

This commit is contained in:
Lloyd
2026-05-12 14:18:33 +01:00
parent eb5d971c72
commit 66532a0647
80 changed files with 689 additions and 114 deletions
+29
View File
@@ -207,6 +207,35 @@ gps:
time_sync_min_offset_seconds: 1.0
time_sync_min_valid_year: 2020
# Sensor plug-ins (polled in background and exposed under /api/stats -> sensors)
sensors:
# Master switch for sensor subsystem
enabled: true
# Poll interval for all configured sensors
poll_interval_seconds: 10.0
# If true, missing Python packages for a sensor may be installed at runtime
# using "python -m pip install ...".
auto_install_packages: true
# Sensor definitions. Add one item per sensor instance.
definitions:
# Built-in sensor: host CPU/memory/disk/network stats
- type: hardware_stats
name: system-health
enabled: true
# Example INA219 sensor (commented out by default)
# - type: ina219
# name: power-monitor
# enabled: true
# auto_install_packages: true
# settings:
# i2c_address: 64 # 0x40 in decimal
# max_expected_amps: 2.0
# shunt_ohms: 0.1
# Mesh Network Configuration
mesh:
# Unscoped flood policy - controls whether the repeater allows or denies unscoped flooding
+8
View File
@@ -136,6 +136,14 @@ def load_config(config_path: Optional[str] = None) -> Dict[str, Any]:
"persist_gps_fix_interval_seconds": 600.0,
}
if "sensors" not in config:
config["sensors"] = {
"enabled": False,
"poll_interval_seconds": 30.0,
"auto_install_packages": False,
"definitions": [],
}
# Ensure repeater.security exists with defaults for upgrades from older configs
if "repeater" not in config:
config["repeater"] = {}
+19
View File
@@ -24,6 +24,7 @@ from repeater.handler_helpers import (
)
from repeater.identity_manager import IdentityManager
from repeater.packet_router import PacketRouter
from repeater.sensors import SensorManager
from repeater.web.http_server import HTTPStatsServer, _log_buffer
logger = logging.getLogger("RepeaterDaemon")
@@ -51,6 +52,7 @@ class RepeaterDaemon:
self.protocol_request_helper = None
self.glass_handler = None
self.gps_service = None
self.sensor_manager = None
self.acl = None
self.router = None
self.companion_bridges: dict[int, object] = {}
@@ -264,6 +266,13 @@ class RepeaterDaemon:
)
logger.info("Config manager initialized")
self.sensor_manager = SensorManager(self.config)
self.sensor_manager.start()
if self.sensor_manager.get_summary().get("loaded", 0):
logger.info("Sensor manager initialized")
else:
logger.info("No configured sensors loaded")
self.gps_service = GPSService(
self.config,
location_update_callback=self._update_repeater_location_from_gps,
@@ -932,6 +941,9 @@ class RepeaterDaemon:
if self.gps_service:
stats["gps"] = self.gps_service.get_summary()
if self.sensor_manager:
stats["sensors"] = self.sensor_manager.get_summary()
return stats
async def _get_companion_stats(self, stats_type: int) -> dict:
@@ -1154,6 +1166,13 @@ class RepeaterDaemon:
except Exception as e:
logger.warning(f"Error stopping Glass handler: {e}")
# Stop sensor manager.
if self.sensor_manager:
try:
self.sensor_manager.stop()
except Exception as e:
logger.warning(f"Error stopping sensor manager: {e}")
# Stop GPS diagnostics.
if self.gps_service:
try:
+12
View File
@@ -0,0 +1,12 @@
from .base import SensorBase
from .manager import SensorManager
from .registry import SensorRegistry
# HardwareStatsSensor is optional (requires psutil); import directly if needed:
# from repeater.sensors.hardware_stats import HardwareStatsSensor
__all__ = [
"SensorBase",
"SensorManager",
"SensorRegistry",
]
+118
View File
@@ -0,0 +1,118 @@
from __future__ import annotations
import importlib.util
import logging
import subprocess
import sys
from abc import ABC, abstractmethod
from datetime import datetime, timezone
from typing import Any, Dict, Iterable, Optional, Tuple
class SensorBase(ABC):
"""Base class for lightweight sensor plug-ins."""
sensor_type = "sensor"
def __init__(self, name: str, config: Optional[Dict[str, Any]] = None, log: Optional[logging.Logger] = None):
self.name = name
self.config = config or {}
self.settings = self.config.get("settings", {}) if isinstance(self.config, dict) else {}
self.enabled = bool(self.config.get("enabled", True)) if isinstance(self.config, dict) else True
self.log = log or logging.getLogger(self.__class__.__name__)
@abstractmethod
def _read(self) -> Dict[str, Any]:
"""Read the sensor and return a raw payload dictionary."""
def read(self) -> Dict[str, Any]:
timestamp = datetime.now(timezone.utc).isoformat()
if not self.enabled:
return self._result(ok=False, timestamp=timestamp, error="disabled")
try:
data = self._read()
except Exception as exc:
self.log.warning("Sensor read failed for %s: %s", self.name, exc)
return self._result(ok=False, timestamp=timestamp, error=f"{type(exc).__name__}: {exc}")
if data is None:
data = {}
if not isinstance(data, dict):
data = {"value": data}
return self._result(ok=True, timestamp=timestamp, data=data)
def _result(
self,
*,
ok: bool,
timestamp: str,
data: Optional[Dict[str, Any]] = None,
error: Optional[str] = None,
) -> Dict[str, Any]:
result: Dict[str, Any] = {
"name": self.name,
"type": self.sensor_type,
"ok": ok,
"timestamp": timestamp,
"data": data or {},
}
if error:
result["error"] = error
return result
def _auto_install_enabled(self) -> bool:
"""Return true when package auto-install is enabled for this sensor."""
raw = self.config.get("auto_install_packages") if isinstance(self.config, dict) else False
return bool(raw)
def ensure_python_modules(self, modules: Iterable[Tuple[str, str]]) -> bool:
"""Ensure Python modules are importable; optionally install via pip if missing.
modules: iterable of (import_name, pip_package_name).
"""
missing: list[Tuple[str, str]] = []
for import_name, package_name in modules:
if importlib.util.find_spec(import_name) is None:
missing.append((import_name, package_name))
if not missing:
return True
if not self._auto_install_enabled():
names = ", ".join(pkg for _, pkg in missing)
self.log.warning(
"Missing sensor dependencies for %s: %s (set auto_install_packages=true to install automatically)",
self.name,
names,
)
return False
for import_name, package_name in missing:
self.log.info("Installing missing dependency for %s: %s", self.name, package_name)
result = subprocess.run(
[sys.executable, "-m", "pip", "install", package_name],
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
self.log.warning(
"Failed installing %s for %s: %s",
package_name,
self.name,
(result.stderr or result.stdout or "unknown error").strip(),
)
return False
if importlib.util.find_spec(import_name) is None:
self.log.warning(
"Dependency %s installed but module %s still unavailable for %s",
package_name,
import_name,
self.name,
)
return False
return True
+20
View File
@@ -0,0 +1,20 @@
from __future__ import annotations
from typing import Any, Dict
from repeater.data_acquisition.hardware_stats import HardwareStatsCollector
from .base import SensorBase
from .registry import SensorRegistry
@SensorRegistry.register("hardware_stats")
class HardwareStatsSensor(SensorBase):
sensor_type = "hardware_stats"
def __init__(self, name: str, config: Dict[str, Any] | None = None, log=None):
super().__init__(name=name, config=config, log=log)
self.collector = HardwareStatsCollector()
def _read(self) -> Dict[str, Any]:
return self.collector.get_stats()
+91
View File
@@ -0,0 +1,91 @@
"""
INA219 current/voltage/power monitor sensor plug-in.
Requires: pip install adafruit-circuitpython-ina219
Config example:
- type: ina219
name: "power_monitor"
enabled: true
auto_install_packages: false
settings:
i2c_address: 0x40 # Default INA219 I2C address
max_expected_amps: 2.0
shunt_ohms: 0.1 # 0.1 Ohm shunt resistor
"""
from __future__ import annotations
from typing import Any, Dict, Optional
from .base import SensorBase
from .registry import SensorRegistry
@SensorRegistry.register("ina219")
class INA219Sensor(SensorBase):
sensor_type = "ina219"
def __init__(self, name: str, config: Optional[Dict[str, Any]] = None, log=None):
super().__init__(name=name, config=config, log=log)
self.i2c_address = self.settings.get("i2c_address", 0x40)
self.max_expected_amps = float(self.settings.get("max_expected_amps", 2.0))
self.shunt_ohms = float(self.settings.get("shunt_ohms", 0.1))
self.available = False
if not self.ensure_python_modules(
[
("board", "board"),
("busio", "adafruit-blinka"),
("adafruit_ina219", "adafruit-circuitpython-ina219"),
]
):
return
try:
import board # type: ignore[import-not-found]
import busio # type: ignore[import-not-found]
from adafruit_ina219 import Adafruit_INA219 # type: ignore[import-not-found]
# Create I2C interface
i2c = busio.I2C(board.SCL, board.SDA)
# Create sensor object
self.ina219 = Adafruit_INA219(
i2c_addr=self.i2c_address,
i2c=i2c,
)
# Configure for expected current range
self.ina219.set_calibration_32V_2A() if self.max_expected_amps <= 2.0 else None
self.available = True
self.log.info(
"INA219 initialized (addr=0x%02X, shunt=%.3fΩ, max_A=%.1f)",
self.i2c_address,
self.shunt_ohms,
self.max_expected_amps,
)
except Exception as exc:
self.log.warning(
"INA219 init failed (addr=0x%02X): %s",
self.i2c_address,
exc,
)
self.available = False
def _read(self) -> Dict[str, Any]:
"""Read voltage, current, and power from INA219."""
if not self.available:
raise RuntimeError("INA219 device not available")
try:
return {
"bus_voltage_v": round(self.ina219.bus_voltage, 3),
"shunt_voltage_v": round(self.ina219.shunt_voltage, 4),
"current_ma": round(self.ina219.current, 2),
"power_mw": round(self.ina219.power, 2),
}
except Exception as exc:
raise RuntimeError(f"INA219 read failed: {exc}") from exc
+176
View File
@@ -0,0 +1,176 @@
from __future__ import annotations
import importlib
import logging
import threading
from typing import Any, Dict, List, Optional
from .registry import SensorRegistry
class SensorManager:
"""Load and read sensor plug-ins declared in config with background polling."""
def __init__(
self,
config: Dict[str, Any],
*,
log: Optional[logging.Logger] = None,
registry: type[SensorRegistry] = SensorRegistry,
):
self.config = config if isinstance(config, dict) else {}
self.log = log or logging.getLogger(self.__class__.__name__)
self.registry = registry
self.sensors = []
# Background polling
self._poll_thread: Optional[threading.Thread] = None
self._stop_event = threading.Event()
self._latest_readings: List[Dict[str, Any]] = []
self._readings_lock = threading.RLock()
self._running = False
self.reload()
def _get_sensor_definitions(self) -> List[Dict[str, Any]]:
"""Extract sensor definitions from config."""
section = self.config.get("sensors", {})
if not isinstance(section, dict):
return []
global_auto_install = bool(section.get("auto_install_packages", False))
definitions = section.get("definitions") or section.get("sensors")
if not isinstance(definitions, list):
return []
out: List[Dict[str, Any]] = []
for d in definitions:
if not isinstance(d, dict):
continue
entry = dict(d)
entry.setdefault("auto_install_packages", global_auto_install)
out.append(entry)
return out
def _load_sensor_module(self, sensor_type: str) -> None:
"""Import sensor module so @SensorRegistry.register decorators execute."""
module_name = str(sensor_type).strip().lower().replace("-", "_")
package_name = __name__.rsplit(".", 1)[0]
importlib.import_module(f"{package_name}.{module_name}")
def reload(self) -> None:
self.sensors = []
for definition in self._get_sensor_definitions():
if not definition.get("enabled", True):
continue
sensor_type = definition.get("type")
name = definition.get("name") or str(sensor_type or "sensor")
if not sensor_type:
self.log.warning("Skipping sensor definition %r: missing type", name)
continue
try:
self._load_sensor_module(sensor_type)
sensor = self.registry.create(sensor_type, name=name, config=definition)
except Exception as exc:
self.log.warning("Skipping sensor %r of type %r: %s", name, sensor_type, exc)
continue
self.sensors.append(sensor)
def start(self) -> None:
if self._running:
return
self.reload()
# Start background polling thread if enabled and sensors exist
section = self.config.get("sensors", {})
if not isinstance(section, dict) or not section.get("enabled", False):
self.log.debug("Sensor manager disabled in config")
return
if not self.sensors:
self.log.debug("No sensors loaded; skipping background polling")
return
self._stop_event.clear()
self._poll_thread = threading.Thread(
target=self._poll_loop, name="sensor-manager", daemon=True
)
self._poll_thread.start()
self._running = True
self.log.info("Sensor manager polling started (%d sensors)", len(self.sensors))
def stop(self) -> None:
if not self._running:
return
self._stop_event.set()
if self._poll_thread and self._poll_thread.is_alive():
self._poll_thread.join(timeout=2.0)
self._running = False
self.log.info("Sensor manager polling stopped")
def read_all(self) -> List[Dict[str, Any]]:
readings: List[Dict[str, Any]] = []
for sensor in self.sensors:
try:
readings.append(sensor.read())
except Exception as exc:
self.log.warning("Sensor manager caught read error for %s: %s", sensor.name, exc)
readings.append(
{
"name": sensor.name,
"type": getattr(sensor, "sensor_type", "sensor"),
"ok": False,
"timestamp": None,
"data": {},
"error": f"{type(exc).__name__}: {exc}",
}
)
return readings
def _poll_loop(self) -> None:
"""Background thread: poll sensors at configured interval and cache readings."""
section = self.config.get("sensors", {})
poll_interval = 30.0
if isinstance(section, dict):
try:
poll_interval = float(section.get("poll_interval_seconds", 30.0))
except (TypeError, ValueError):
pass
self.log.debug("Sensor polling loop started (interval=%.1f sec)", poll_interval)
while not self._stop_event.is_set():
try:
readings = self.read_all()
with self._readings_lock:
self._latest_readings = readings
except Exception as exc:
self.log.warning("Sensor poll cycle failed: %s", exc)
# Wait for next poll or stop signal
self._stop_event.wait(poll_interval)
self.log.debug("Sensor polling loop stopped")
def get_summary(self) -> Dict[str, Any]:
section = self.config.get("sensors", {})
poll_interval = 30.0
if isinstance(section, dict):
try:
poll_interval = float(section.get("poll_interval_seconds", 30.0))
except (TypeError, ValueError):
pass
# Get cached readings (or empty list if not running)
with self._readings_lock:
readings = list(self._latest_readings) if self._latest_readings else []
return {
"enabled": bool(isinstance(section, dict) and section.get("enabled", False)),
"poll_interval_seconds": poll_interval,
"configured": len(self._get_sensor_definitions()),
"loaded": len(self.sensors),
"running": self._running,
"readings": readings,
}
+38
View File
@@ -0,0 +1,38 @@
from __future__ import annotations
from typing import Any, Callable, Dict, Optional
from .base import SensorBase
class SensorRegistry:
_factories: Dict[str, Callable[..., SensorBase]] = {}
@classmethod
def register(cls, sensor_type: str, factory: Optional[Callable[..., SensorBase]] = None):
"""Register a sensor factory or class under a sensor type."""
key = str(sensor_type).strip().lower()
def _decorator(target):
cls._factories[key] = target
return target
if factory is not None:
cls._factories[key] = factory
return factory
return _decorator
@classmethod
def create(cls, sensor_type: str, name: str, config: Optional[Dict[str, Any]] = None, **kwargs) -> SensorBase:
key = str(sensor_type).strip().lower()
factory = cls._factories.get(key)
if factory is None:
raise ValueError(f"Unknown sensor type: {sensor_type}")
if isinstance(factory, type) and issubclass(factory, SensorBase):
return factory(name=name, config=config, **kwargs)
return factory(name=name, config=config, **kwargs)
@classmethod
def available_types(cls) -> list[str]:
return sorted(cls._factories)
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
.ml-0[data-v-b31cd389]{margin-left:0}.ml-4[data-v-b31cd389]{margin-left:1rem}.ml-8[data-v-b31cd389]{margin-left:2rem}.ml-12[data-v-b31cd389]{margin-left:3rem}.ml-16[data-v-b31cd389]{margin-left:4rem}.ml-20[data-v-b31cd389]{margin-left:5rem}.ml-24[data-v-b31cd389]{margin-left:6rem}.ml-28[data-v-b31cd389]{margin-left:7rem}.ml-32[data-v-b31cd389]{margin-left:8rem}.dropdown-enter-active[data-v-f23b2d7c],.dropdown-leave-active[data-v-f23b2d7c]{transition:opacity .12s,transform .12s}.dropdown-enter-from[data-v-f23b2d7c],.dropdown-leave-to[data-v-f23b2d7c]{opacity:0;transform:translateY(-4px)}.expand-enter-active[data-v-00e540ed],.expand-leave-active[data-v-00e540ed]{transition:all .2s;overflow:hidden}.expand-enter-from[data-v-00e540ed],.expand-leave-to[data-v-00e540ed]{opacity:0;max-height:0}.expand-enter-to[data-v-00e540ed],.expand-leave-from[data-v-00e540ed]{opacity:1;max-height:2000px}.tab-fade-left[data-v-b3e04bd5]{background:linear-gradient(to right, var(--color-surface) 30%, transparent)}.tab-fade-right[data-v-b3e04bd5]{background:linear-gradient(to left, var(--color-surface) 30%, transparent)}.tab-fade-enter-active[data-v-b3e04bd5],.tab-fade-leave-active[data-v-b3e04bd5]{transition:opacity .2s}.tab-fade-enter-from[data-v-b3e04bd5],.tab-fade-leave-to[data-v-b3e04bd5]{opacity:0}
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
.ml-0[data-v-b31cd389]{margin-left:0}.ml-4[data-v-b31cd389]{margin-left:1rem}.ml-8[data-v-b31cd389]{margin-left:2rem}.ml-12[data-v-b31cd389]{margin-left:3rem}.ml-16[data-v-b31cd389]{margin-left:4rem}.ml-20[data-v-b31cd389]{margin-left:5rem}.ml-24[data-v-b31cd389]{margin-left:6rem}.ml-28[data-v-b31cd389]{margin-left:7rem}.ml-32[data-v-b31cd389]{margin-left:8rem}.dropdown-enter-active[data-v-f23b2d7c],.dropdown-leave-active[data-v-f23b2d7c]{transition:opacity .12s,transform .12s}.dropdown-enter-from[data-v-f23b2d7c],.dropdown-leave-to[data-v-f23b2d7c]{opacity:0;transform:translateY(-4px)}.expand-enter-active[data-v-218b986b],.expand-leave-active[data-v-218b986b]{transition:all .2s;overflow:hidden}.expand-enter-from[data-v-218b986b],.expand-leave-to[data-v-218b986b]{opacity:0;max-height:0}.expand-enter-to[data-v-218b986b],.expand-leave-from[data-v-218b986b]{opacity:1;max-height:2000px}.tab-fade-left[data-v-3a402227]{background:linear-gradient(to right, var(--color-surface) 30%, transparent)}.tab-fade-right[data-v-3a402227]{background:linear-gradient(to left, var(--color-surface) 30%, transparent)}.tab-fade-enter-active[data-v-3a402227],.tab-fade-leave-active[data-v-3a402227]{transition:opacity .2s}.tab-fade-enter-from[data-v-3a402227],.tab-fade-leave-to[data-v-3a402227]{opacity:0}
@@ -1 +1 @@
import{E as e,c as t,g as n,gt as r,i,l as a,mt as o,s,u as c}from"./runtime-core.esm-bundler-BiwffnQU.js";import{_ as l}from"./index-Bwi6QvPf.js";var u={class:`modal-card max-w-md`},d={class:`flex items-center justify-between mb-4`},f={class:`text-xl font-semibold text-content-primary dark:text-content-primary`},p={class:`mb-6`},m={key:0,class:`w-6 h-6`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},h={key:1,class:`w-6 h-6`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},g={key:2,class:`w-6 h-6`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},_={class:`text-content-secondary dark:text-content-primary/80 text-base leading-relaxed`},v={class:`flex gap-3`},y=n({__name:`ConfirmDialog`,props:{show:{type:Boolean},title:{default:`Confirm Action`},message:{},confirmText:{default:`Confirm`},cancelText:{default:`Cancel`},variant:{default:`warning`}},emits:[`close`,`confirm`],setup(n,{emit:y}){let b=n,x=y,S={danger:`bg-red-100 dark:bg-red-500/20 border-red-500/30 text-red-600 dark:text-red-400`,warning:`bg-yellow-100 dark:bg-yellow-500/20 border-yellow-500/30 text-yellow-600 dark:text-yellow-400`,info:`bg-blue-500/20 border-blue-500/30 text-blue-600 dark:text-blue-400`},C={danger:`bg-red-500 hover:bg-red-600`,warning:`bg-yellow-500 hover:bg-yellow-600`,info:`bg-blue-500 hover:bg-blue-600`};return(n,y)=>(e(),t(i,{to:`body`},[b.show?(e(),c(`div`,{key:0,onClick:y[3]||=l(e=>x(`close`),[`self`]),class:`modal-backdrop`},[s(`div`,u,[s(`div`,d,[s(`h3`,f,r(b.title),1),s(`button`,{onClick:y[0]||=e=>x(`close`),class:`text-content-secondary dark:text-content-muted hover:text-content-primary dark:hover:text-content-primary transition-colors`},[...y[4]||=[s(`svg`,{class:`w-6 h-6`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},[s(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M6 18L18 6M6 6l12 12`})],-1)]])]),s(`div`,p,[s(`div`,{class:o([`inline-flex p-3 rounded-xl mb-4`,S[b.variant]])},[b.variant===`danger`?(e(),c(`svg`,m,[...y[5]||=[s(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z`},null,-1)]])):b.variant===`warning`?(e(),c(`svg`,h,[...y[6]||=[s(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z`},null,-1)]])):(e(),c(`svg`,g,[...y[7]||=[s(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z`},null,-1)]]))],2),s(`p`,_,r(b.message),1)]),s(`div`,v,[s(`button`,{onClick:y[1]||=e=>x(`close`),class:`flex-1 px-4 py-3 rounded-xl bg-background-mute dark:bg-white/5 hover:bg-stroke-subtle dark:hover:bg-white/10 text-content-primary dark:text-content-primary transition-all duration-200 border border-stroke-subtle dark:border-stroke/10`},r(b.cancelText),1),s(`button`,{onClick:y[2]||=e=>x(`confirm`),class:o([`flex-1 px-4 py-3 rounded-xl text-white transition-all duration-200`,C[b.variant]])},r(b.confirmText),3)])])])):a(``,!0)]))}});export{y as t};
import{E as e,c as t,g as n,gt as r,i,l as a,mt as o,s,u as c}from"./runtime-core.esm-bundler-BiwffnQU.js";import{_ as l}from"./index-T8oWhJNd.js";var u={class:`modal-card max-w-md`},d={class:`flex items-center justify-between mb-4`},f={class:`text-xl font-semibold text-content-primary dark:text-content-primary`},p={class:`mb-6`},m={key:0,class:`w-6 h-6`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},h={key:1,class:`w-6 h-6`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},g={key:2,class:`w-6 h-6`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},_={class:`text-content-secondary dark:text-content-primary/80 text-base leading-relaxed`},v={class:`flex gap-3`},y=n({__name:`ConfirmDialog`,props:{show:{type:Boolean},title:{default:`Confirm Action`},message:{},confirmText:{default:`Confirm`},cancelText:{default:`Cancel`},variant:{default:`warning`}},emits:[`close`,`confirm`],setup(n,{emit:y}){let b=n,x=y,S={danger:`bg-red-100 dark:bg-red-500/20 border-red-500/30 text-red-600 dark:text-red-400`,warning:`bg-yellow-100 dark:bg-yellow-500/20 border-yellow-500/30 text-yellow-600 dark:text-yellow-400`,info:`bg-blue-500/20 border-blue-500/30 text-blue-600 dark:text-blue-400`},C={danger:`bg-red-500 hover:bg-red-600`,warning:`bg-yellow-500 hover:bg-yellow-600`,info:`bg-blue-500 hover:bg-blue-600`};return(n,y)=>(e(),t(i,{to:`body`},[b.show?(e(),c(`div`,{key:0,onClick:y[3]||=l(e=>x(`close`),[`self`]),class:`modal-backdrop`},[s(`div`,u,[s(`div`,d,[s(`h3`,f,r(b.title),1),s(`button`,{onClick:y[0]||=e=>x(`close`),class:`text-content-secondary dark:text-content-muted hover:text-content-primary dark:hover:text-content-primary transition-colors`},[...y[4]||=[s(`svg`,{class:`w-6 h-6`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},[s(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M6 18L18 6M6 6l12 12`})],-1)]])]),s(`div`,p,[s(`div`,{class:o([`inline-flex p-3 rounded-xl mb-4`,S[b.variant]])},[b.variant===`danger`?(e(),c(`svg`,m,[...y[5]||=[s(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z`},null,-1)]])):b.variant===`warning`?(e(),c(`svg`,h,[...y[6]||=[s(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z`},null,-1)]])):(e(),c(`svg`,g,[...y[7]||=[s(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z`},null,-1)]]))],2),s(`p`,_,r(b.message),1)]),s(`div`,v,[s(`button`,{onClick:y[1]||=e=>x(`close`),class:`flex-1 px-4 py-3 rounded-xl bg-background-mute dark:bg-white/5 hover:bg-stroke-subtle dark:hover:bg-white/10 text-content-primary dark:text-content-primary transition-all duration-200 border border-stroke-subtle dark:border-stroke/10`},r(b.cancelText),1),s(`button`,{onClick:y[2]||=e=>x(`confirm`),class:o([`flex-1 px-4 py-3 rounded-xl text-white transition-all duration-200`,C[b.variant]])},r(b.confirmText),3)])])])):a(``,!0)]))}});export{y as t};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
.bg-gradient-light[data-v-a26c2589]{background:linear-gradient(#0d73774d,#aae8e833)}.bg-gradient-dark[data-v-a26c2589]{background:linear-gradient(#aae8e82e,#0d73771a)}.login-card[data-v-a26c2589]{-webkit-backdrop-filter:blur(40px)saturate(180%);background:#ffffffd9}.dark .login-card[data-v-a26c2589]{background:#1a1e1fcc}.input-glass[data-v-a26c2589]{-webkit-backdrop-filter:blur(20px);background:#ffffffe6;border:1px solid #d1d5db}.dark .input-glass[data-v-a26c2589]{background:#ffffff0d;border-color:#ffffff1a}.input-glass[data-v-a26c2589]:focus{background:#fff}.dark .input-glass[data-v-a26c2589]:focus{background:#ffffff1a}.input-glass[data-v-a26c2589]:focus{box-shadow:0 0 0 1px #aae8e833,0 0 20px #aae8e826,inset 0 1px #ffffff1a}.input-glow[data-v-a26c2589]{opacity:0;transition:opacity .3s;box-shadow:inset 0 1px #ffffff0d}.input-glass:focus+.input-glow[data-v-a26c2589]{opacity:1;box-shadow:0 0 20px #aae8e833,inset 0 1px #ffffff1a}.button-glass[data-v-a26c2589]{-webkit-backdrop-filter:blur(20px);position:relative}.button-glass[data-v-a26c2589]:before{content:"";-webkit-mask-composite:xor;background:linear-gradient(90deg,#0000 0%,#aae8e84d 50%,#0000 100%);border-radius:12px;padding:1px;transition:transform 1s;position:absolute;inset:0;transform:translate(-100%);-webkit-mask-image:linear-gradient(#fff 0 0),linear-gradient(#fff 0 0);-webkit-mask-position:0 0,0 0;-webkit-mask-size:auto,auto;-webkit-mask-repeat:repeat,repeat;-webkit-mask-clip:content-box,border-box;-webkit-mask-origin:content-box,border-box;-webkit-mask-composite:xor;mask-composite:exclude;-webkit-mask-source-type:auto,auto;mask-mode:match-source,match-source}.button-glass[data-v-a26c2589]:hover:not(:disabled):before{transform:translate(100%)}.button-glass[data-v-a26c2589]{box-shadow:0 0 0 1px #aae8e833,0 4px 16px #0003,inset 0 1px #ffffff1a}.button-glass[data-v-a26c2589]:hover:not(:disabled){box-shadow:0 0 0 1px #aae8e866,0 0 30px #aae8e84d,0 4px 20px #0000004d,inset 0 1px #ffffff26}@keyframes float-a26c2589{0%,to{transform:translateY(0)}50%{transform:translateY(-10px)}}@keyframes pulse-slow-a26c2589{0%,to{opacity:.8;transform:scale(1)}50%{opacity:.6;transform:scale(1.05)}}@keyframes pulse-slower-a26c2589{0%,to{opacity:.75;transform:scale(1)}50%{opacity:.5;transform:scale(1.08)}}@keyframes pulse-slowest-a26c2589{0%,to{opacity:.8;transform:scale(1)}50%{opacity:.6;transform:scale(1.06)}}.animate-pulse-slow[data-v-a26c2589]{animation:8s ease-in-out infinite pulse-slow-a26c2589}.animate-pulse-slower[data-v-a26c2589]{animation:10s ease-in-out infinite pulse-slower-a26c2589}.animate-pulse-slowest[data-v-a26c2589]{animation:12s ease-in-out infinite pulse-slowest-a26c2589}@keyframes shake-a26c2589{0%,to{transform:translate(0)}10%,30%,50%,70%,90%{transform:translate(-5px)}20%,40%,60%,80%{transform:translate(5px)}}.animate-shake[data-v-a26c2589]{animation:.5s ease-in-out shake-a26c2589}.form-group[data-v-a26c2589]{position:relative}.form-group:hover label[data-v-a26c2589]{color:#aae8e8e6;transition:color .3s}
@@ -0,0 +1 @@
.bg-gradient-light[data-v-9842a645]{background:linear-gradient(#0d73774d,#aae8e833)}.bg-gradient-dark[data-v-9842a645]{background:linear-gradient(#aae8e82e,#0d73771a)}.login-card[data-v-9842a645]{-webkit-backdrop-filter:blur(40px)saturate(180%);background:#ffffffd9}.dark .login-card[data-v-9842a645]{background:#1a1e1fcc}.input-glass[data-v-9842a645]{-webkit-backdrop-filter:blur(20px);background:#ffffffe6;border:1px solid #d1d5db}.dark .input-glass[data-v-9842a645]{background:#ffffff0d;border-color:#ffffff1a}.input-glass[data-v-9842a645]:focus{background:#fff}.dark .input-glass[data-v-9842a645]:focus{background:#ffffff1a}.input-glass[data-v-9842a645]:focus{box-shadow:0 0 0 1px #aae8e833,0 0 20px #aae8e826,inset 0 1px #ffffff1a}.input-glow[data-v-9842a645]{opacity:0;transition:opacity .3s;box-shadow:inset 0 1px #ffffff0d}.input-glass:focus+.input-glow[data-v-9842a645]{opacity:1;box-shadow:0 0 20px #aae8e833,inset 0 1px #ffffff1a}.button-glass[data-v-9842a645]{-webkit-backdrop-filter:blur(20px);position:relative}.button-glass[data-v-9842a645]:before{content:"";-webkit-mask-composite:xor;background:linear-gradient(90deg,#0000 0%,#aae8e84d 50%,#0000 100%);border-radius:12px;padding:1px;transition:transform 1s;position:absolute;inset:0;transform:translate(-100%);-webkit-mask-image:linear-gradient(#fff 0 0),linear-gradient(#fff 0 0);-webkit-mask-position:0 0,0 0;-webkit-mask-size:auto,auto;-webkit-mask-repeat:repeat,repeat;-webkit-mask-clip:content-box,border-box;-webkit-mask-origin:content-box,border-box;-webkit-mask-composite:xor;mask-composite:exclude;-webkit-mask-source-type:auto,auto;mask-mode:match-source,match-source}.button-glass[data-v-9842a645]:hover:not(:disabled):before{transform:translate(100%)}.button-glass[data-v-9842a645]{box-shadow:0 0 0 1px #aae8e833,0 4px 16px #0003,inset 0 1px #ffffff1a}.button-glass[data-v-9842a645]:hover:not(:disabled){box-shadow:0 0 0 1px #aae8e866,0 0 30px #aae8e84d,0 4px 20px #0000004d,inset 0 1px #ffffff26}@keyframes float-9842a645{0%,to{transform:translateY(0)}50%{transform:translateY(-10px)}}@keyframes pulse-slow-9842a645{0%,to{opacity:.8;transform:scale(1)}50%{opacity:.6;transform:scale(1.05)}}@keyframes pulse-slower-9842a645{0%,to{opacity:.75;transform:scale(1)}50%{opacity:.5;transform:scale(1.08)}}@keyframes pulse-slowest-9842a645{0%,to{opacity:.8;transform:scale(1)}50%{opacity:.6;transform:scale(1.06)}}.animate-pulse-slow[data-v-9842a645]{animation:8s ease-in-out infinite pulse-slow-9842a645}.animate-pulse-slower[data-v-9842a645]{animation:10s ease-in-out infinite pulse-slower-9842a645}.animate-pulse-slowest[data-v-9842a645]{animation:12s ease-in-out infinite pulse-slowest-9842a645}@keyframes shake-9842a645{0%,to{transform:translate(0)}10%,30%,50%,70%,90%{transform:translate(-5px)}20%,40%,60%,80%{transform:translate(5px)}}.animate-shake[data-v-9842a645]{animation:.5s ease-in-out shake-9842a645}.form-group[data-v-9842a645]{position:relative}.form-group:hover label[data-v-9842a645]{color:#aae8e8e6;transition:color .3s}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{E as e,c as t,g as n,gt as r,i,l as a,mt as o,s,u as c}from"./runtime-core.esm-bundler-BiwffnQU.js";import{_ as l}from"./index-Bwi6QvPf.js";var u={class:`modal-card max-w-md`},d={class:`mb-6`},f={key:0,class:`w-6 h-6`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},p={key:1,class:`w-6 h-6`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},m={key:2,class:`w-6 h-6`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},h={class:`text-content-secondary dark:text-content-primary/80 text-base leading-relaxed`},g={class:`flex`},_=n({__name:`MessageDialog`,props:{show:{type:Boolean},message:{},variant:{default:`success`}},emits:[`close`],setup(n,{emit:_}){let v=n,y=_,b={success:`bg-green-100 dark:bg-green-500/20 border-green-600/40 dark:border-green-500/30 text-green-600 dark:text-green-400`,error:`bg-red-100 dark:bg-red-500/20 border-red-500/30 text-red-600 dark:text-red-400`,info:`bg-blue-500/20 border-blue-500/30 text-blue-600 dark:text-blue-400`},x={success:`bg-green-500 hover:bg-green-600`,error:`bg-red-500 hover:bg-red-600`,info:`bg-blue-500 hover:bg-blue-600`};return(n,_)=>(e(),t(i,{to:`body`},[v.show?(e(),c(`div`,{key:0,onClick:_[1]||=l(e=>y(`close`),[`self`]),class:`modal-backdrop`},[s(`div`,u,[s(`div`,d,[s(`div`,{class:o([`inline-flex p-3 rounded-xl mb-4`,b[v.variant]])},[v.variant===`success`?(e(),c(`svg`,f,[..._[2]||=[s(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M5 13l4 4L19 7`},null,-1)]])):v.variant===`error`?(e(),c(`svg`,p,[..._[3]||=[s(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M6 18L18 6M6 6l12 12`},null,-1)]])):(e(),c(`svg`,m,[..._[4]||=[s(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z`},null,-1)]]))],2),s(`p`,h,r(v.message),1)]),s(`div`,g,[s(`button`,{onClick:_[0]||=e=>y(`close`),class:o([`flex-1 px-4 py-3 rounded-xl text-white transition-all duration-200`,x[v.variant]])},` OK `,2)])])])):a(``,!0)]))}});export{_ as t};
import{E as e,c as t,g as n,gt as r,i,l as a,mt as o,s,u as c}from"./runtime-core.esm-bundler-BiwffnQU.js";import{_ as l}from"./index-T8oWhJNd.js";var u={class:`modal-card max-w-md`},d={class:`mb-6`},f={key:0,class:`w-6 h-6`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},p={key:1,class:`w-6 h-6`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},m={key:2,class:`w-6 h-6`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},h={class:`text-content-secondary dark:text-content-primary/80 text-base leading-relaxed`},g={class:`flex`},_=n({__name:`MessageDialog`,props:{show:{type:Boolean},message:{},variant:{default:`success`}},emits:[`close`],setup(n,{emit:_}){let v=n,y=_,b={success:`bg-green-100 dark:bg-green-500/20 border-green-600/40 dark:border-green-500/30 text-green-600 dark:text-green-400`,error:`bg-red-100 dark:bg-red-500/20 border-red-500/30 text-red-600 dark:text-red-400`,info:`bg-blue-500/20 border-blue-500/30 text-blue-600 dark:text-blue-400`},x={success:`bg-green-500 hover:bg-green-600`,error:`bg-red-500 hover:bg-red-600`,info:`bg-blue-500 hover:bg-blue-600`};return(n,_)=>(e(),t(i,{to:`body`},[v.show?(e(),c(`div`,{key:0,onClick:_[1]||=l(e=>y(`close`),[`self`]),class:`modal-backdrop`},[s(`div`,u,[s(`div`,d,[s(`div`,{class:o([`inline-flex p-3 rounded-xl mb-4`,b[v.variant]])},[v.variant===`success`?(e(),c(`svg`,f,[..._[2]||=[s(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M5 13l4 4L19 7`},null,-1)]])):v.variant===`error`?(e(),c(`svg`,p,[..._[3]||=[s(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M6 18L18 6M6 6l12 12`},null,-1)]])):(e(),c(`svg`,m,[..._[4]||=[s(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z`},null,-1)]]))],2),s(`p`,h,r(v.message),1)]),s(`div`,g,[s(`button`,{onClick:_[0]||=e=>y(`close`),class:o([`flex-1 px-4 py-3 rounded-xl text-white transition-all duration-200`,x[v.variant]])},` OK `,2)])])])):a(``,!0)]))}});export{_ as t};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{o as e}from"./index-T8oWhJNd.js";export{e as default};
@@ -1 +0,0 @@
import{o as e}from"./index-Bwi6QvPf.js";export{e as default};
@@ -1 +1 @@
import{E as e,H as t,N as n,S as r,c as i,g as a,gt as o,i as s,j as c,l,m as u,r as d,s as f,u as p}from"./runtime-core.esm-bundler-BiwffnQU.js";import{t as m}from"./api-DRn_D8oU.js";import{_ as h,t as g,u as _}from"./index-Bwi6QvPf.js";var v={class:`bg-white dark:bg-[var(--color-surface-elevated)] rounded-xl shadow-xl border border-stroke-subtle dark:border-stroke/20 p-6 max-w-md w-full mx-4`},y={key:0,class:`flex flex-col items-center gap-5 py-2`},b={class:`flex items-start gap-3 mb-4`},x={class:`text-base font-semibold text-content-primary dark:text-content-primary`},S={class:`mt-1 text-sm text-content-secondary dark:text-content-muted`},C=50,w=5,T=a({__name:`RestartModal`,props:{modelValue:{type:Boolean},message:{},title:{default:`Service Restart Required`}},emits:[`update:modelValue`],setup(a,{emit:T}){let E=a,D=T,O=t(!1),k=t(!1),A=null,j=0,M=0;function N(){O.value&&!k.value||(O.value=!1,k.value=!1,A&&=(clearTimeout(A),null),j=0,M=0,D(`update:modelValue`,!1))}async function P(){O.value=!0,k.value=!1;try{await m.post(`/restart_service`,{})}catch{}j=0,M=0,A=setTimeout(F,1e4)}function F(){j++,fetch(`/api/needs_setup`,{method:`GET`}).then(e=>{e.ok?(M++,M>=w?window.location.reload():A=setTimeout(F,1e3)):(M=0,I())}).catch(()=>{M=0,I()})}function I(){j<C?A=setTimeout(F,1e3):(O.value=!1,k.value=!0)}return c(()=>E.modelValue,e=>{e||(O.value=!1,k.value=!1,A&&=(clearTimeout(A),null),j=0,M=0)}),r(()=>{A&&clearTimeout(A)}),(t,r)=>(e(),i(s,{to:`body`},[u(_,{"enter-active-class":`transition-opacity duration-200`,"enter-from-class":`opacity-0`,"leave-active-class":`transition-opacity duration-200`,"leave-to-class":`opacity-0`},{default:n(()=>[a.modelValue?(e(),p(`div`,{key:0,class:`fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm`,onClick:h(N,[`self`])},[f(`div`,v,[O.value?(e(),p(`div`,y,[u(g,{size:`lg`}),r[0]||=f(`div`,{class:`text-center`},[f(`h3`,{class:`text-base font-semibold text-content-primary dark:text-content-primary`},` Restarting… `),f(`p`,{class:`mt-1 text-sm text-content-secondary dark:text-content-muted`},` Please wait while the service restarts. This may take up to a minute. `)],-1)])):k.value?(e(),p(d,{key:1},[r[1]||=f(`div`,{class:`flex items-start gap-3 mb-4`},[f(`div`,{class:`flex-shrink-0 w-10 h-10 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center`},[f(`svg`,{class:`w-5 h-5 text-red-600 dark:text-red-400`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},[f(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z`})])]),f(`div`,null,[f(`h3`,{class:`text-base font-semibold text-content-primary dark:text-content-primary`},` Service Did Not Restart `),f(`p`,{class:`mt-1 text-sm text-content-secondary dark:text-content-muted`},` The service did not respond after 60 seconds. Please log into the device and check the system logs. `)])],-1),f(`div`,{class:`flex justify-end`},[f(`button`,{onClick:N,class:`px-3 sm:px-4 py-2 bg-background-mute dark:bg-white/5 hover:bg-stroke-subtle dark:hover:bg-white/10 text-content-primary dark:text-content-primary rounded-lg border border-stroke-subtle dark:border-stroke/20 transition-colors text-sm`},` Dismiss `)])],64)):(e(),p(d,{key:2},[f(`div`,b,[r[2]||=f(`div`,{class:`flex-shrink-0 w-10 h-10 rounded-full bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center`},[f(`svg`,{class:`w-5 h-5 text-amber-600 dark:text-amber-400`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},[f(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z`})])],-1),f(`div`,null,[f(`h3`,x,o(a.title),1),f(`p`,S,o(a.message),1)])]),f(`div`,{class:`flex justify-end gap-2`},[f(`button`,{onClick:N,class:`px-3 sm:px-4 py-2 bg-background-mute dark:bg-white/5 hover:bg-stroke-subtle dark:hover:bg-white/10 text-content-primary dark:text-content-primary rounded-lg border border-stroke-subtle dark:border-stroke/20 transition-colors text-sm`},` Cancel `),f(`button`,{onClick:P,class:`px-3 sm:px-4 py-2 bg-primary/20 hover:bg-primary/30 text-content-primary dark:text-content-primary rounded-lg border border-primary/50 transition-colors text-sm`},` Restart `)])],64))])])):l(``,!0)]),_:1})]))}});export{T as t};
import{E as e,H as t,N as n,S as r,c as i,g as a,gt as o,i as s,j as c,l,m as u,r as d,s as f,u as p}from"./runtime-core.esm-bundler-BiwffnQU.js";import{t as m}from"./api-D2kWYBuR.js";import{_ as h,n as g,u as _}from"./index-T8oWhJNd.js";var v={class:`bg-white dark:bg-[var(--color-surface-elevated)] rounded-xl shadow-xl border border-stroke-subtle dark:border-stroke/20 p-6 max-w-md w-full mx-4`},y={key:0,class:`flex flex-col items-center gap-5 py-2`},b={class:`flex items-start gap-3 mb-4`},x={class:`text-base font-semibold text-content-primary dark:text-content-primary`},S={class:`mt-1 text-sm text-content-secondary dark:text-content-muted`},C=50,w=5,T=a({__name:`RestartModal`,props:{modelValue:{type:Boolean},message:{},title:{default:`Service Restart Required`}},emits:[`update:modelValue`],setup(a,{emit:T}){let E=a,D=T,O=t(!1),k=t(!1),A=null,j=0,M=0;function N(){O.value&&!k.value||(O.value=!1,k.value=!1,A&&=(clearTimeout(A),null),j=0,M=0,D(`update:modelValue`,!1))}async function P(){O.value=!0,k.value=!1;try{await m.post(`/restart_service`,{})}catch{}j=0,M=0,A=setTimeout(F,1e4)}function F(){j++,fetch(`/api/needs_setup`,{method:`GET`}).then(e=>{e.ok?(M++,M>=w?window.location.reload():A=setTimeout(F,1e3)):(M=0,I())}).catch(()=>{M=0,I()})}function I(){j<C?A=setTimeout(F,1e3):(O.value=!1,k.value=!0)}return c(()=>E.modelValue,e=>{e||(O.value=!1,k.value=!1,A&&=(clearTimeout(A),null),j=0,M=0)}),r(()=>{A&&clearTimeout(A)}),(t,r)=>(e(),i(s,{to:`body`},[u(_,{"enter-active-class":`transition-opacity duration-200`,"enter-from-class":`opacity-0`,"leave-active-class":`transition-opacity duration-200`,"leave-to-class":`opacity-0`},{default:n(()=>[a.modelValue?(e(),p(`div`,{key:0,class:`fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm`,onClick:h(N,[`self`])},[f(`div`,v,[O.value?(e(),p(`div`,y,[u(g,{size:`lg`}),r[0]||=f(`div`,{class:`text-center`},[f(`h3`,{class:`text-base font-semibold text-content-primary dark:text-content-primary`},` Restarting… `),f(`p`,{class:`mt-1 text-sm text-content-secondary dark:text-content-muted`},` Please wait while the service restarts. This may take up to a minute. `)],-1)])):k.value?(e(),p(d,{key:1},[r[1]||=f(`div`,{class:`flex items-start gap-3 mb-4`},[f(`div`,{class:`flex-shrink-0 w-10 h-10 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center`},[f(`svg`,{class:`w-5 h-5 text-red-600 dark:text-red-400`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},[f(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z`})])]),f(`div`,null,[f(`h3`,{class:`text-base font-semibold text-content-primary dark:text-content-primary`},` Service Did Not Restart `),f(`p`,{class:`mt-1 text-sm text-content-secondary dark:text-content-muted`},` The service did not respond after 60 seconds. Please log into the device and check the system logs. `)])],-1),f(`div`,{class:`flex justify-end`},[f(`button`,{onClick:N,class:`px-3 sm:px-4 py-2 bg-background-mute dark:bg-white/5 hover:bg-stroke-subtle dark:hover:bg-white/10 text-content-primary dark:text-content-primary rounded-lg border border-stroke-subtle dark:border-stroke/20 transition-colors text-sm`},` Dismiss `)])],64)):(e(),p(d,{key:2},[f(`div`,b,[r[2]||=f(`div`,{class:`flex-shrink-0 w-10 h-10 rounded-full bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center`},[f(`svg`,{class:`w-5 h-5 text-amber-600 dark:text-amber-400`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},[f(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z`})])],-1),f(`div`,null,[f(`h3`,x,o(a.title),1),f(`p`,S,o(a.message),1)])]),f(`div`,{class:`flex justify-end gap-2`},[f(`button`,{onClick:N,class:`px-3 sm:px-4 py-2 bg-background-mute dark:bg-white/5 hover:bg-stroke-subtle dark:hover:bg-white/10 text-content-primary dark:text-content-primary rounded-lg border border-stroke-subtle dark:border-stroke/20 transition-colors text-sm`},` Cancel `),f(`button`,{onClick:P,class:`px-3 sm:px-4 py-2 bg-primary/20 hover:bg-primary/30 text-content-primary dark:text-content-primary rounded-lg border border-primary/50 transition-colors text-sm`},` Restart `)])],64))])])):l(``,!0)]),_:1})]))}});export{T as t};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{E as e,O as t,g as n,gt as r,l as i,mt as a,o,r as s,s as c,u as l}from"./runtime-core.esm-bundler-BiwffnQU.js";import{t as u}from"./system--i3xLloC.js";import{t as d}from"./index-T8oWhJNd.js";var f={class:`space-y-4`},p={class:`glass-card rounded-[15px] p-4 sm:p-6`},m={class:`mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4`},h={class:`text-xs uppercase tracking-wide text-content-muted`},g={class:`mt-2 text-lg font-semibold text-content-heading dark:text-white`},_={key:0,class:`glass-card rounded-[15px] p-5 text-content-muted`},v={class:`flex flex-wrap items-center justify-between gap-3`},y={class:`text-lg font-semibold text-content-heading dark:text-white`},b={class:`text-sm text-content-muted`},x={class:`mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2`},S={class:`text-sm`},C={class:`ml-2 text-content-heading dark:text-white`},w={key:0,class:`text-sm`},T={class:`ml-2 text-red-600 dark:text-red-300`},E={class:`mt-4 overflow-x-auto rounded-[12px] border border-stroke-subtle dark:border-white/10`},D={class:`min-w-full text-sm`},O={class:`px-3 py-2 font-medium text-content-heading dark:text-white`},k={class:`px-3 py-2 text-content-muted break-all`},A={key:0},j={key:1,class:`glass-card rounded-[15px] p-5 text-content-muted`},M=n({name:`SensorsView`,__name:`Sensors`,setup(n){let M=u(),N=o(()=>M.stats?.sensors??null),P=o(()=>N.value?.readings??[]),F=o(()=>{let e=N.value;return e?[{label:`Enabled`,value:e.enabled?`Yes`:`No`},{label:`Running`,value:e.running?`Yes`:`No`},{label:`Configured / Loaded`,value:`${e.configured??0} / ${e.loaded??0}`},{label:`Poll Interval`,value:typeof e.poll_interval_seconds==`number`?`${e.poll_interval_seconds.toFixed(1)}s`:`n/a`}]:[{label:`Enabled`,value:`n/a`},{label:`Running`,value:`n/a`},{label:`Configured`,value:`n/a`},{label:`Poll Interval`,value:`n/a`}]}),I=e=>{if(e==null)return`n/a`;if(typeof e==`boolean`)return e?`true`:`false`;if(typeof e==`number`)return Number.isFinite(e)?String(e):`n/a`;if(typeof e==`string`)return e;try{return JSON.stringify(e)}catch{return String(e)}},L=e=>{if(!e)return`n/a`;let t=new Date(e);return Number.isNaN(t.getTime())?e:t.toLocaleString()},R=async()=>{await M.fetchStats()};return d(async()=>{await M.fetchStats()},{intervalMs:1e4,immediate:!0}),(n,o)=>(e(),l(`div`,f,[c(`div`,p,[c(`div`,{class:`flex items-start justify-between gap-4`},[o[0]||=c(`div`,null,[c(`h1`,{class:`text-xl sm:text-2xl font-semibold text-content-heading dark:text-white`},`Sensors`),c(`p`,{class:`mt-1 text-sm text-content-muted`},` Live sensor summary from the existing stats API. `)],-1),c(`button`,{class:`rounded-[10px] border border-stroke-subtle dark:border-white/10 px-3 py-2 text-sm hover:bg-black/5 dark:hover:bg-white/5`,onClick:R},` Refresh `)]),c(`div`,m,[(e(!0),l(s,null,t(F.value,t=>(e(),l(`div`,{key:t.label,class:`rounded-[12px] border border-stroke-subtle dark:border-white/10 p-3`},[c(`p`,h,r(t.label),1),c(`p`,g,r(t.value),1)]))),128))])]),N.value?i(``,!0):(e(),l(`div`,_,` Sensor data is not available yet. Ensure the repeater has started and stats are loading. `)),(e(!0),l(s,null,t(P.value,(n,u)=>(e(),l(`div`,{key:`${n.name||`sensor`}-${u}`,class:`glass-card rounded-[15px] p-4 sm:p-5`},[c(`div`,v,[c(`div`,null,[c(`h2`,y,r(n.name||`Sensor ${u+1}`),1),c(`p`,b,`Type: `+r(n.type||`unknown`),1)]),c(`span`,{class:a([`rounded-full px-3 py-1 text-xs font-semibold`,n.ok?`bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-300`:`bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-300`])},r(n.ok?`OK`:`Error`),3)]),c(`div`,x,[c(`div`,S,[o[1]||=c(`span`,{class:`text-content-muted`},`Timestamp:`,-1),c(`span`,C,r(L(n.timestamp)),1)]),n.error?(e(),l(`div`,w,[o[2]||=c(`span`,{class:`text-content-muted`},`Error:`,-1),c(`span`,T,r(n.error),1)])):i(``,!0)]),c(`div`,E,[c(`table`,D,[o[4]||=c(`thead`,{class:`bg-black/5 dark:bg-white/5`},[c(`tr`,null,[c(`th`,{class:`px-3 py-2 text-left text-content-muted`},`Field`),c(`th`,{class:`px-3 py-2 text-left text-content-muted`},`Value`)])],-1),c(`tbody`,null,[(e(!0),l(s,null,t(n.data||{},(t,n)=>(e(),l(`tr`,{key:String(n),class:`border-t border-stroke-subtle dark:border-white/10`},[c(`td`,O,r(n),1),c(`td`,k,r(I(t)),1)]))),128)),!n.data||Object.keys(n.data).length===0?(e(),l(`tr`,A,[...o[3]||=[c(`td`,{class:`px-3 py-3 text-content-muted`,colspan:`2`},`No fields in payload`,-1)]])):i(``,!0)])])])]))),128)),N.value&&P.value.length===0?(e(),l(`div`,j,` Sensors are configured but no readings are available yet. `)):i(``,!0)]))}});export{M as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
.glass-card[data-v-c3cf9234]{-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);background:#ffffff0d;border:1px solid #ffffff1a}.modal-enter-active[data-v-c3cf9234],.modal-leave-active[data-v-c3cf9234]{transition:opacity .3s}.modal-enter-from[data-v-c3cf9234],.modal-leave-to[data-v-c3cf9234]{opacity:0}.modal-enter-active .glass-card[data-v-c3cf9234],.modal-leave-active .glass-card[data-v-c3cf9234]{transition:transform .3s}.modal-enter-from .glass-card[data-v-c3cf9234],.modal-leave-to .glass-card[data-v-c3cf9234]{transform:scale(.9)}.slide-enter-active[data-v-c3cf9234],.slide-leave-active[data-v-c3cf9234]{transition:all .3s}.slide-enter-from[data-v-c3cf9234],.slide-leave-to[data-v-c3cf9234]{opacity:0;transform:translateY(-10px)}@keyframes float-slow-c3cf9234{0%,to{opacity:.8;transform:translate(0)scale(1)rotate(-24.22deg)}50%{opacity:.6;transform:translate(20px,-20px)scale(1.05)rotate(-24.22deg)}}@keyframes float-slower-c3cf9234{0%,to{opacity:.75;transform:translate(0)scale(1)rotate(-24.22deg)}50%{opacity:.5;transform:translate(-30px,20px)scale(1.08)rotate(-24.22deg)}}@keyframes float-slowest-c3cf9234{0%,to{opacity:.8;transform:translate(0)scale(1)rotate(-24.22deg)}50%{opacity:.55;transform:translate(25px,25px)scale(1.1)rotate(-24.22deg)}}.animate-pulse-slow[data-v-c3cf9234]{will-change:transform, opacity;animation:15s ease-in-out infinite float-slow-c3cf9234}.animate-pulse-slower[data-v-c3cf9234]{will-change:transform, opacity;animation:18s ease-in-out infinite float-slower-c3cf9234}.animate-pulse-slowest[data-v-c3cf9234]{will-change:transform, opacity;animation:20s ease-in-out infinite float-slowest-c3cf9234}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
.glass-card[data-v-923df164]{-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);background:#ffffff0d;border:1px solid #ffffff1a}.modal-enter-active[data-v-923df164],.modal-leave-active[data-v-923df164]{transition:opacity .3s}.modal-enter-from[data-v-923df164],.modal-leave-to[data-v-923df164]{opacity:0}.modal-enter-active .glass-card[data-v-923df164],.modal-leave-active .glass-card[data-v-923df164]{transition:transform .3s}.modal-enter-from .glass-card[data-v-923df164],.modal-leave-to .glass-card[data-v-923df164]{transform:scale(.9)}.slide-enter-active[data-v-923df164],.slide-leave-active[data-v-923df164]{transition:all .3s}.slide-enter-from[data-v-923df164],.slide-leave-to[data-v-923df164]{opacity:0;transform:translateY(-10px)}@keyframes float-slow-923df164{0%,to{opacity:.8;transform:translate(0)scale(1)rotate(-24.22deg)}50%{opacity:.6;transform:translate(20px,-20px)scale(1.05)rotate(-24.22deg)}}@keyframes float-slower-923df164{0%,to{opacity:.75;transform:translate(0)scale(1)rotate(-24.22deg)}50%{opacity:.5;transform:translate(-30px,20px)scale(1.08)rotate(-24.22deg)}}@keyframes float-slowest-923df164{0%,to{opacity:.8;transform:translate(0)scale(1)rotate(-24.22deg)}50%{opacity:.55;transform:translate(25px,25px)scale(1.1)rotate(-24.22deg)}}.animate-pulse-slow[data-v-923df164]{will-change:transform, opacity;animation:15s ease-in-out infinite float-slow-923df164}.animate-pulse-slower[data-v-923df164]{will-change:transform, opacity;animation:18s ease-in-out infinite float-slower-923df164}.animate-pulse-slowest[data-v-923df164]{will-change:transform, opacity;animation:20s ease-in-out infinite float-slowest-923df164}
@@ -0,0 +1 @@
.plotly-chart[data-v-3467abe2]{background:0 0!important}
@@ -1 +0,0 @@
.plotly-chart[data-v-41e22744]{background:0 0!important}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
.glass-card[data-v-f0591faf]{-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);background:#ffffffbf;border:1px solid #0000000f;box-shadow:0 2px 8px #0000000a}.dark .glass-card[data-v-f0591faf]{box-shadow:none;background:#0000004d;border:1px solid #ffffff1a}.chart-updating[data-v-f0591faf]{animation:.8s ease-in-out subtle-pulse-f0591faf}@keyframes subtle-pulse-f0591faf{0%{transform:scale(1)}50%{transform:scale(1.02)}to{transform:scale(1)}}.chart-container[data-v-f0591faf]{transition:all .3s;position:relative}.chart-container[data-v-f0591faf]:hover{background:#0000000a}.dark .chart-container[data-v-f0591faf]:hover{background:#ffffff14}.process-row[data-v-f0591faf]{transition:all .3s}.process-row[data-v-f0591faf]:hover{background:#00000005;transform:translate(2px)}.dark .process-row[data-v-f0591faf]:hover{background:#ffffff0d}.process-row-enter-active[data-v-f0591faf],.process-row-leave-active[data-v-f0591faf]{transition:all .4s}.process-row-enter-from[data-v-f0591faf]{opacity:0;transform:translateY(-10px)scale(.95)}.process-row-leave-to[data-v-f0591faf]{opacity:0;transform:translateY(10px)scale(.95)}.process-row-move[data-v-f0591faf]{transition:transform .4s}.cpu-value[data-v-f0591faf],.memory-value[data-v-f0591faf]{border-radius:4px;padding:2px 6px;transition:all .3s}.cpu-value[data-v-f0591faf]:hover,.memory-value[data-v-f0591faf]:hover{background:#f59e0b1a;transform:scale(1.05)}@keyframes value-update-f0591faf{0%{background:#f59e0b4d}to{background:0 0}}.value-updated[data-v-f0591faf]{animation:.6s ease-out value-update-f0591faf}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
.glass-card[data-v-1b55faef]{-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);background:#ffffffbf;border:1px solid #0000000f;box-shadow:0 2px 8px #0000000a}.dark .glass-card[data-v-1b55faef]{box-shadow:none;background:#0000004d;border:1px solid #ffffff1a}.chart-updating[data-v-1b55faef]{animation:.8s ease-in-out subtle-pulse-1b55faef}@keyframes subtle-pulse-1b55faef{0%{transform:scale(1)}50%{transform:scale(1.02)}to{transform:scale(1)}}.chart-container[data-v-1b55faef]{transition:all .3s;position:relative}.chart-container[data-v-1b55faef]:hover{background:#0000000a}.dark .chart-container[data-v-1b55faef]:hover{background:#ffffff14}.process-row[data-v-1b55faef]{transition:all .3s}.process-row[data-v-1b55faef]:hover{background:#00000005;transform:translate(2px)}.dark .process-row[data-v-1b55faef]:hover{background:#ffffff0d}.process-row-enter-active[data-v-1b55faef],.process-row-leave-active[data-v-1b55faef]{transition:all .4s}.process-row-enter-from[data-v-1b55faef]{opacity:0;transform:translateY(-10px)scale(.95)}.process-row-leave-to[data-v-1b55faef]{opacity:0;transform:translateY(10px)scale(.95)}.process-row-move[data-v-1b55faef]{transition:transform .4s}.cpu-value[data-v-1b55faef],.memory-value[data-v-1b55faef]{border-radius:4px;padding:2px 6px;transition:all .3s}.cpu-value[data-v-1b55faef]:hover,.memory-value[data-v-1b55faef]:hover{background:#f59e0b1a;transform:scale(1.05)}@keyframes value-update-1b55faef{0%{background:#f59e0b4d}to{background:0 0}}.value-updated[data-v-1b55faef]{animation:.6s ease-out value-update-1b55faef}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{H as e,V as t,o as n}from"./runtime-core.esm-bundler-BiwffnQU.js";import{n as r}from"./pinia-IvTtuI9d.js";import{t as i}from"./api-DRn_D8oU.js";import{t as a}from"./packets-D3t50H8n.js";import{t as o}from"./system-DN2LtoaR.js";var s={0:`Unknown`,1:`Chat Node`,2:`Repeater`,3:`Room Server`,4:`Hybrid Node`},c=r(`neighbors`,()=>{let t=e({}),r=e(!1),a=e(null),o=e(48),c=n(()=>Object.values(t.value).flat()),l=n(()=>c.value.length);function u(e=10*6e4){return a.value===null?!0:Date.now()-a.value>e}async function d(e=o.value){r.value=!0,o.value=e;let n=Object.entries(s),c=await Promise.allSettled(n.map(async([t,n])=>{try{let r=[],a=0,o=0;for(;o<200;){let t=await i.get(`/adverts_by_contact_type?contact_type=${encodeURIComponent(n)}&hours=${e}&limit=500&offset=${a}`),s=t.success&&Array.isArray(t.data)?t.data:[];if(s.length===0||(r.push(...s),s.length<500))break;a+=500,o+=1}return{typeKey:t,adverts:r}}catch{return{typeKey:t,adverts:[]}}})),l={};for(let e of c)e.status===`fulfilled`&&e.value.adverts.length>0&&(l[e.value.typeKey]=e.value.adverts);t.value=l,a.value=Date.now(),r.value=!1}function f(){t.value={},r.value=!1,a.value=null,o.value=48}return{advertsByType:t,isLoading:r,lastFetched:a,currentHours:o,allAdverts:c,totalCount:l,isStale:u,fetchAll:d,reset:f}}),l={stats:3e4,packetStats:6e4,noiseFloor:15e3,recentPackets:3e4,sparklines:3e5,advertTier:6e4,neighbors:10*6e4},u=r(`dataService`,()=>{let n=o(),r=a(),s=c(),u=e({currentTier:`unknown`,advertsAllowed:0,advertsDropped:0,activePenalties:0}),d=e(!1),f=e(null),p=t({stats:`pending`,packetStats:`pending`,noiseFloor:`pending`,recentPackets:`pending`,sparklines:`pending`,advertTier:`pending`,neighbors:`pending`}),m=new Map,h=new Map,g=[],_=!1;async function v(e,t=2){for(let n=0;n<t;n++)try{return await e()}catch(e){if(n===t-1)throw e;await new Promise(e=>setTimeout(e,500*2**n))}throw Error(`unreachable`)}async function y(){try{let e=(await i.get(`/advert_rate_limit_stats`))?.data;u.value={currentTier:typeof e?.adaptive?.current_tier==`string`?e.adaptive.current_tier:`unknown`,advertsAllowed:e?.stats?.adverts_allowed||0,advertsDropped:e?.stats?.adverts_dropped||0,activePenalties:Object.keys(e?.active_penalties||{}).length},m.set(`advertTier`,Date.now())}catch{}}async function b(e){if(e===`neighbors`){if(!s.isStale())return}else{let t=m.get(e);if(t!==void 0&&Date.now()-t<l[e])return}let t=h.get(e);if(t)return t;let i;switch(e){case`stats`:i=n.fetchStats().then(()=>{m.set(`stats`,Date.now())});break;case`packetStats`:i=r.fetchPacketStats({hours:24}).then(()=>{m.set(`packetStats`,Date.now())});break;case`noiseFloor`:i=r.fetchNoiseFloorHistory({hours:24,limit:500}).then(()=>{m.set(`noiseFloor`,Date.now())});break;case`recentPackets`:i=r.fetchRecentPackets({limit:100}).then(()=>{m.set(`recentPackets`,Date.now())});break;case`sparklines`:i=r.initializeSparklineHistory().then(()=>{m.set(`sparklines`,Date.now())});break;case`advertTier`:i=y();break;case`neighbors`:i=s.fetchAll(s.currentHours).then(()=>{});break}return h.set(e,i),i.finally(()=>h.delete(e)),i}async function x(e,t){p[e]=`loading`;try{await t(),p[e]=`done`}catch{p[e]=`error`}}async function S(){if(!_){_=!0,d.value=!0,p.stats=`loading`,f.value=`requesting`;try{await v(()=>n.fetchStats({onFirstByte:()=>{f.value=`reading`}})),m.set(`stats`,Date.now()),p.stats=`done`}catch{p.stats=`error`,console.error(`[DataService] Failed to fetch stats after retries`)}finally{f.value=null}await Promise.allSettled([x(`packetStats`,()=>r.fetchPacketStats({hours:24}).then(()=>{m.set(`packetStats`,Date.now())})),x(`noiseFloor`,()=>r.fetchNoiseFloorHistory({hours:24,limit:500}).then(()=>{m.set(`noiseFloor`,Date.now())})),x(`recentPackets`,()=>r.fetchRecentPackets({limit:100}).then(()=>{m.set(`recentPackets`,Date.now())}))]),await Promise.allSettled([x(`sparklines`,()=>r.initializeSparklineHistory().then(()=>{m.set(`sparklines`,Date.now())})),x(`advertTier`,()=>y()),x(`neighbors`,()=>s.fetchAll(s.currentHours).then(()=>{}))]),d.value=!1,C()}}function C(){T(),g.push(window.setInterval(()=>void b(`advertTier`),3e4)),g.push(window.setInterval(()=>void b(`packetStats`),6e4)),g.push(window.setInterval(()=>void b(`noiseFloor`),15e3)),g.push(window.setInterval(()=>void b(`sparklines`),3e5)),g.push(window.setInterval(()=>{let e=n.lastUpdated?.getTime()??0;Date.now()-e>25e3&&b(`stats`)},3e4))}async function w(){await new Promise(e=>setTimeout(e,3e3)),await Promise.allSettled([b(`stats`),b(`packetStats`),b(`recentPackets`)])}function T(){for(let e of g)clearInterval(e);g=[]}function E(){T(),_=!1,m.clear(),h.clear(),d.value=!1,Object.keys(p).forEach(e=>{p[e]=`pending`}),u.value={currentTier:`unknown`,advertsAllowed:0,advertsDropped:0,activePenalties:0}}return{advertTier:u,isBootstrapping:d,statsSubStatus:f,loadProgress:p,bootstrap:S,ensure:b,onReconnect:w,stopPolling:T,reset:E}});export{s as n,c as r,u as t};
import{H as e,V as t,o as n}from"./runtime-core.esm-bundler-BiwffnQU.js";import{n as r}from"./pinia-IvTtuI9d.js";import{t as i}from"./api-D2kWYBuR.js";import{t as a}from"./packets-BzRHEedS.js";import{t as o}from"./system--i3xLloC.js";var s={0:`Unknown`,1:`Chat Node`,2:`Repeater`,3:`Room Server`,4:`Hybrid Node`},c=r(`neighbors`,()=>{let t=e({}),r=e(!1),a=e(null),o=e(48),c=n(()=>Object.values(t.value).flat()),l=n(()=>c.value.length);function u(e=10*6e4){return a.value===null?!0:Date.now()-a.value>e}async function d(e=o.value){r.value=!0,o.value=e;let n=Object.entries(s),c=await Promise.allSettled(n.map(async([t,n])=>{try{let r=[],a=0,o=0;for(;o<200;){let t=await i.get(`/adverts_by_contact_type?contact_type=${encodeURIComponent(n)}&hours=${e}&limit=500&offset=${a}`),s=t.success&&Array.isArray(t.data)?t.data:[];if(s.length===0||(r.push(...s),s.length<500))break;a+=500,o+=1}return{typeKey:t,adverts:r}}catch{return{typeKey:t,adverts:[]}}})),l={};for(let e of c)e.status===`fulfilled`&&e.value.adverts.length>0&&(l[e.value.typeKey]=e.value.adverts);t.value=l,a.value=Date.now(),r.value=!1}function f(){t.value={},r.value=!1,a.value=null,o.value=48}return{advertsByType:t,isLoading:r,lastFetched:a,currentHours:o,allAdverts:c,totalCount:l,isStale:u,fetchAll:d,reset:f}}),l={stats:3e4,packetStats:6e4,noiseFloor:15e3,recentPackets:3e4,sparklines:3e5,advertTier:6e4,neighbors:10*6e4},u=r(`dataService`,()=>{let n=o(),r=a(),s=c(),u=e({currentTier:`unknown`,advertsAllowed:0,advertsDropped:0,activePenalties:0}),d=e(!1),f=e(null),p=t({stats:`pending`,packetStats:`pending`,noiseFloor:`pending`,recentPackets:`pending`,sparklines:`pending`,advertTier:`pending`,neighbors:`pending`}),m=new Map,h=new Map,g=[],_=!1;async function v(e,t=2){for(let n=0;n<t;n++)try{return await e()}catch(e){if(n===t-1)throw e;await new Promise(e=>setTimeout(e,500*2**n))}throw Error(`unreachable`)}async function y(){try{let e=(await i.get(`/advert_rate_limit_stats`))?.data;u.value={currentTier:typeof e?.adaptive?.current_tier==`string`?e.adaptive.current_tier:`unknown`,advertsAllowed:e?.stats?.adverts_allowed||0,advertsDropped:e?.stats?.adverts_dropped||0,activePenalties:Object.keys(e?.active_penalties||{}).length},m.set(`advertTier`,Date.now())}catch{}}async function b(e){if(e===`neighbors`){if(!s.isStale())return}else{let t=m.get(e);if(t!==void 0&&Date.now()-t<l[e])return}let t=h.get(e);if(t)return t;let i;switch(e){case`stats`:i=n.fetchStats().then(()=>{m.set(`stats`,Date.now())});break;case`packetStats`:i=r.fetchPacketStats({hours:24}).then(()=>{m.set(`packetStats`,Date.now())});break;case`noiseFloor`:i=r.fetchNoiseFloorHistory({hours:24,limit:500}).then(()=>{m.set(`noiseFloor`,Date.now())});break;case`recentPackets`:i=r.fetchRecentPackets({limit:100}).then(()=>{m.set(`recentPackets`,Date.now())});break;case`sparklines`:i=r.initializeSparklineHistory().then(()=>{m.set(`sparklines`,Date.now())});break;case`advertTier`:i=y();break;case`neighbors`:i=s.fetchAll(s.currentHours).then(()=>{});break}return h.set(e,i),i.finally(()=>h.delete(e)),i}async function x(e,t){p[e]=`loading`;try{await t(),p[e]=`done`}catch{p[e]=`error`}}async function S(){if(!_){_=!0,d.value=!0,p.stats=`loading`,f.value=`requesting`;try{await v(()=>n.fetchStats({onFirstByte:()=>{f.value=`reading`}})),m.set(`stats`,Date.now()),p.stats=`done`}catch{p.stats=`error`,console.error(`[DataService] Failed to fetch stats after retries`)}finally{f.value=null}await Promise.allSettled([x(`packetStats`,()=>r.fetchPacketStats({hours:24}).then(()=>{m.set(`packetStats`,Date.now())})),x(`noiseFloor`,()=>r.fetchNoiseFloorHistory({hours:24,limit:500}).then(()=>{m.set(`noiseFloor`,Date.now())})),x(`recentPackets`,()=>r.fetchRecentPackets({limit:100}).then(()=>{m.set(`recentPackets`,Date.now())}))]),await Promise.allSettled([x(`sparklines`,()=>r.initializeSparklineHistory().then(()=>{m.set(`sparklines`,Date.now())})),x(`advertTier`,()=>y()),x(`neighbors`,()=>s.fetchAll(s.currentHours).then(()=>{}))]),d.value=!1,C()}}function C(){T(),g.push(window.setInterval(()=>void b(`advertTier`),3e4)),g.push(window.setInterval(()=>void b(`packetStats`),6e4)),g.push(window.setInterval(()=>void b(`noiseFloor`),15e3)),g.push(window.setInterval(()=>void b(`sparklines`),3e5)),g.push(window.setInterval(()=>{let e=n.lastUpdated?.getTime()??0;Date.now()-e>25e3&&b(`stats`)},3e4))}async function w(){await new Promise(e=>setTimeout(e,3e3)),await Promise.allSettled([b(`stats`),b(`packetStats`),b(`recentPackets`)])}function T(){for(let e of g)clearInterval(e);g=[]}function E(){T(),_=!1,m.clear(),h.clear(),d.value=!1,Object.keys(p).forEach(e=>{p[e]=`pending`}),u.value={currentTier:`unknown`,advertsAllowed:0,advertsDropped:0,activePenalties:0}}return{advertTier:u,isBootstrapping:d,statsSubStatus:f,loadProgress:p,bootstrap:S,ensure:b,onReconnect:w,stopPolling:T,reset:E}});export{s as n,c as r,u as t};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 612 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 806 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
import{t as e}from"./packets-D3t50H8n.js";export{e as usePacketStore};
@@ -0,0 +1 @@
import{t as e}from"./packets-BzRHEedS.js";export{e as usePacketStore};
@@ -1 +1 @@
import{H as e,o as t}from"./runtime-core.esm-bundler-BiwffnQU.js";import{n}from"./pinia-IvTtuI9d.js";import{n as r,t as i}from"./api-DRn_D8oU.js";import{t as a}from"./packets-D3t50H8n.js";var o=`pymc_config_cache`;function s(){try{let e=sessionStorage.getItem(o);return e?JSON.parse(e):null}catch{return null}}function c(e){if(e)try{sessionStorage.setItem(o,JSON.stringify(e))}catch{}}function l(){try{sessionStorage.removeItem(o)}catch{}}var u=n(`system`,()=>{let n=s(),o=e(n?{config:n}:null),u=e(!1),d=e(null),f=e(null),p=e(`forward`),m=e(!0),h=e(0),g=e(10),_=e(!1),v=t(()=>o.value?.config?.node_name??`Unknown`),y=t(()=>{let e=o.value?.public_key;return!e||e===`Unknown`?`Unknown`:e.length>=16?`${e.slice(0,8)} ... ${e.slice(-8)}`:`${e}`}),b=t(()=>o.value!==null),x=t(()=>o.value?.version??`Unknown`),S=t(()=>o.value?.core_version??`Unknown`),C=t(()=>o.value?.noise_floor_dbm??null),w=t(()=>g.value>0?Math.min(h.value/g.value*100,100):0),T=t(()=>p.value===`no_tx`?{text:`No TX`,title:`No repeat, no local TX; adverts skipped`}:p.value===`monitor`?{text:`Monitor Mode`,title:`Monitoring only - not forwarding packets`}:m.value?{text:`Active`,title:`Forwarding with duty cycle enforcement`}:{text:`No Limits`,title:`Forwarding without duty cycle enforcement`}),E=t(()=>({mode:p.value})),D=t(()=>m.value?{active:!0,warning:!1}:{active:!1,warning:!0}),O=e=>{_.value=e},k=null;async function A(e){return k===null?(k=(async()=>{try{u.value=!0,d.value=null;let t=new AbortController,n=15e3,i=window.setTimeout(()=>t.abort(),n),s=!1,l=()=>{s||(s=!0,e?.onFirstByte?.()),clearTimeout(i),i=window.setTimeout(()=>t.abort(),n)},p;try{p=await r.get(`/stats`,{signal:t.signal,onDownloadProgress:l,timeout:0})}finally{clearTimeout(i)}let m=p.data,h;if(m.success&&m.data)h=m.data;else if(m&&`version`in m)h=m;else throw Error(m.error||`Failed to fetch stats`);return o.value=h,f.value=new Date,j(h),c(h.config),a().systemStats=h,h}catch(e){throw d.value=e instanceof Error?e.message:`Unknown error occurred`,console.error(`Error fetching stats:`,e),e}finally{u.value=!1}})(),k.finally(()=>{k=null}),k):k}function j(e){if(e.config){let t=e.config.repeater?.mode;t===`forward`||t===`monitor`||t===`no_tx`?p.value=t:t!==void 0&&(p.value=`forward`);let n=e.config.duty_cycle;if(n){m.value=n.enforcement_enabled!==!1;let e=n.max_airtime_percent;typeof e==`number`?g.value=e:e&&typeof e==`object`&&`parsedValue`in e&&(g.value=e.parsedValue||10)}}let t=e.utilization_percent;typeof t==`number`?h.value=t:t&&typeof t==`object`&&`parsedValue`in t&&(h.value=t.parsedValue||0)}async function M(e){try{let t=await i.post(`/set_mode`,{mode:e});if(t.success)return p.value=e,!0;throw Error(t.error||`Failed to set mode`)}catch(e){throw d.value=e instanceof Error?e.message:`Unknown error occurred`,console.error(`Error setting mode:`,e),e}}async function N(e){try{let t=await i.post(`/set_duty_cycle`,{enabled:e});if(t.success)return m.value=e,!0;throw Error(t.error||`Failed to set duty cycle`)}catch(e){throw d.value=e instanceof Error?e.message:`Unknown error occurred`,console.error(`Error setting duty cycle:`,e),e}}async function P(){try{let e=await i.post(`/send_advert`,{},{timeout:1e4});if(e.success)return!0;throw Error(e.error||`Failed to send advert`)}catch(e){throw d.value=e instanceof Error?e.message:`Unknown error occurred`,console.error(`Error sending advert:`,e),e}}async function F(){return await N(!m.value)}function I(e){o.value?(e.uptime_seconds!==void 0&&(o.value.uptime_seconds=e.uptime_seconds),e.noise_floor_dbm!==void 0&&(o.value.noise_floor_dbm=e.noise_floor_dbm)):o.value=e,f.value=new Date,j(e)}async function L(e=5e3,t=!1){t||await A();let n=null;return t||(n=setInterval(async()=>{try{await A()}catch(e){console.error(`Auto-refresh error:`,e)}},e)),()=>{n&&clearInterval(n)}}function R(){o.value=null,d.value=null,f.value=null,u.value=!1,p.value=`forward`,m.value=!0,h.value=0,g.value=10,l()}return{stats:o,isLoading:u,error:d,lastUpdated:f,currentMode:p,dutyCycleEnabled:m,dutyCycleUtilization:h,dutyCycleMax:g,cadCalibrationRunning:_,nodeName:v,pubKey:y,hasStats:b,version:x,coreVersion:S,noiseFloorDbm:C,dutyCyclePercentage:w,statusBadge:T,modeButtonState:E,dutyCycleButtonState:D,fetchStats:A,setMode:M,setDutyCycle:N,sendAdvert:P,toggleDutyCycle:F,startAutoRefresh:L,updateRealtimeStats:I,reset:R,setCadCalibrationRunning:O}});export{u as t};
import{H as e,o as t}from"./runtime-core.esm-bundler-BiwffnQU.js";import{n}from"./pinia-IvTtuI9d.js";import{n as r,t as i}from"./api-D2kWYBuR.js";import{t as a}from"./packets-BzRHEedS.js";var o=`pymc_config_cache`;function s(){try{let e=sessionStorage.getItem(o);return e?JSON.parse(e):null}catch{return null}}function c(e){if(e)try{sessionStorage.setItem(o,JSON.stringify(e))}catch{}}function l(){try{sessionStorage.removeItem(o)}catch{}}var u=n(`system`,()=>{let n=s(),o=e(n?{config:n}:null),u=e(!1),d=e(null),f=e(null),p=e(`forward`),m=e(!0),h=e(0),g=e(10),_=e(!1),v=t(()=>o.value?.config?.node_name??`Unknown`),y=t(()=>{let e=o.value?.public_key;return!e||e===`Unknown`?`Unknown`:e.length>=16?`${e.slice(0,8)} ... ${e.slice(-8)}`:`${e}`}),b=t(()=>o.value!==null),x=t(()=>o.value?.version??`Unknown`),S=t(()=>o.value?.core_version??`Unknown`),C=t(()=>o.value?.noise_floor_dbm??null),w=t(()=>g.value>0?Math.min(h.value/g.value*100,100):0),T=t(()=>p.value===`no_tx`?{text:`No TX`,title:`No repeat, no local TX; adverts skipped`}:p.value===`monitor`?{text:`Monitor Mode`,title:`Monitoring only - not forwarding packets`}:m.value?{text:`Active`,title:`Forwarding with duty cycle enforcement`}:{text:`No Limits`,title:`Forwarding without duty cycle enforcement`}),E=t(()=>({mode:p.value})),D=t(()=>m.value?{active:!0,warning:!1}:{active:!1,warning:!0}),O=e=>{_.value=e},k=null;async function A(e){return k===null?(k=(async()=>{try{u.value=!0,d.value=null;let t=new AbortController,n=15e3,i=window.setTimeout(()=>t.abort(),n),s=!1,l=()=>{s||(s=!0,e?.onFirstByte?.()),clearTimeout(i),i=window.setTimeout(()=>t.abort(),n)},p;try{p=await r.get(`/stats`,{signal:t.signal,onDownloadProgress:l,timeout:0})}finally{clearTimeout(i)}let m=p.data,h;if(m.success&&m.data)h=m.data;else if(m&&`version`in m)h=m;else throw Error(m.error||`Failed to fetch stats`);return o.value=h,f.value=new Date,j(h),c(h.config),a().systemStats=h,h}catch(e){throw d.value=e instanceof Error?e.message:`Unknown error occurred`,console.error(`Error fetching stats:`,e),e}finally{u.value=!1}})(),k.finally(()=>{k=null}),k):k}function j(e){if(e.config){let t=e.config.repeater?.mode;t===`forward`||t===`monitor`||t===`no_tx`?p.value=t:t!==void 0&&(p.value=`forward`);let n=e.config.duty_cycle;if(n){m.value=n.enforcement_enabled!==!1;let e=n.max_airtime_percent;typeof e==`number`?g.value=e:e&&typeof e==`object`&&`parsedValue`in e&&(g.value=e.parsedValue||10)}}let t=e.utilization_percent;typeof t==`number`?h.value=t:t&&typeof t==`object`&&`parsedValue`in t&&(h.value=t.parsedValue||0)}async function M(e){try{let t=await i.post(`/set_mode`,{mode:e});if(t.success)return p.value=e,!0;throw Error(t.error||`Failed to set mode`)}catch(e){throw d.value=e instanceof Error?e.message:`Unknown error occurred`,console.error(`Error setting mode:`,e),e}}async function N(e){try{let t=await i.post(`/set_duty_cycle`,{enabled:e});if(t.success)return m.value=e,!0;throw Error(t.error||`Failed to set duty cycle`)}catch(e){throw d.value=e instanceof Error?e.message:`Unknown error occurred`,console.error(`Error setting duty cycle:`,e),e}}async function P(){try{let e=await i.post(`/send_advert`,{},{timeout:1e4});if(e.success)return!0;throw Error(e.error||`Failed to send advert`)}catch(e){throw d.value=e instanceof Error?e.message:`Unknown error occurred`,console.error(`Error sending advert:`,e),e}}async function F(){return await N(!m.value)}function I(e){o.value?(e.uptime_seconds!==void 0&&(o.value.uptime_seconds=e.uptime_seconds),e.noise_floor_dbm!==void 0&&(o.value.noise_floor_dbm=e.noise_floor_dbm)):o.value=e,f.value=new Date,j(e)}async function L(e=5e3,t=!1){t||await A();let n=null;return t||(n=setInterval(async()=>{try{await A()}catch(e){console.error(`Auto-refresh error:`,e)}},e)),()=>{n&&clearInterval(n)}}function R(){o.value=null,d.value=null,f.value=null,u.value=!1,p.value=`forward`,m.value=!0,h.value=0,g.value=10,l()}return{stats:o,isLoading:u,error:d,lastUpdated:f,currentMode:p,dutyCycleEnabled:m,dutyCycleUtilization:h,dutyCycleMax:g,cadCalibrationRunning:_,nodeName:v,pubKey:y,hasStats:b,version:x,coreVersion:S,noiseFloorDbm:C,dutyCyclePercentage:w,statusBadge:T,modeButtonState:E,dutyCycleButtonState:D,fetchStats:A,setMode:M,setDutyCycle:N,sendAdvert:P,toggleDutyCycle:F,startAutoRefresh:L,updateRealtimeStats:I,reset:R,setCadCalibrationRunning:O}});export{u as t};
@@ -1 +0,0 @@
import{t as e}from"./system-DN2LtoaR.js";export{e as useSystemStore};
@@ -0,0 +1 @@
import{t as e}from"./system--i3xLloC.js";export{e as useSystemStore};
@@ -1 +1 @@
import{o as e}from"./runtime-core.esm-bundler-BiwffnQU.js";import{t}from"./system-DN2LtoaR.js";var n={7:-7.5,8:-10,9:-12.5,10:-15,11:-17.5,12:-20},r=-116,i=8,a=5;function o(e,t){return e-t}function s(e){return n[e]??n[i]}function c(e,t){let n=t+a;if(e<=t){let n=e<=t-5?0:1;return{bars:n,color:`text-red-600 dark:text-red-400`,snr:e,quality:n===0?`None`:`Poor`}}if(e<n){let n=(e-t)/a<.5?2:3;return{bars:n,color:n===2?`text-orange-600 dark:text-orange-400`:`text-yellow-600 dark:text-yellow-400`,snr:e,quality:`Fair`}}let r=e-n>=10?5:4;return{bars:r,color:r===5?`text-green-600 dark:text-green-400`:`text-green-600 dark:text-green-300`,snr:e,quality:r===5?`Excellent`:`Good`}}function l(){let n=t(),a=e(()=>n.noiseFloorDbm??r),l=e(()=>n.stats?.config?.radio?.spreading_factor??i),u=e(()=>s(l.value));return{getSignalQuality:e=>{if(!e||e>0||e<-120)return{bars:0,color:`text-gray-400 dark:text-gray-500`,snr:-999,quality:`None`};let t=o(e,a.value);return c(Math.max(-30,Math.min(20,t)),u.value)},noiseFloor:a,spreadingFactor:l,minSNR:u}}export{l as t};
import{o as e}from"./runtime-core.esm-bundler-BiwffnQU.js";import{t}from"./system--i3xLloC.js";var n={7:-7.5,8:-10,9:-12.5,10:-15,11:-17.5,12:-20},r=-116,i=8,a=5;function o(e,t){return e-t}function s(e){return n[e]??n[i]}function c(e,t){let n=t+a;if(e<=t){let n=e<=t-5?0:1;return{bars:n,color:`text-red-600 dark:text-red-400`,snr:e,quality:n===0?`None`:`Poor`}}if(e<n){let n=(e-t)/a<.5?2:3;return{bars:n,color:n===2?`text-orange-600 dark:text-orange-400`:`text-yellow-600 dark:text-yellow-400`,snr:e,quality:`Fair`}}let r=e-n>=10?5:4;return{bars:r,color:r===5?`text-green-600 dark:text-green-400`:`text-green-600 dark:text-green-300`,snr:e,quality:r===5?`Excellent`:`Good`}}function l(){let n=t(),a=e(()=>n.noiseFloorDbm??r),l=e(()=>n.stats?.config?.radio?.spreading_factor??i),u=e(()=>s(l.value));return{getSignalQuality:e=>{if(!e||e>0||e<-120)return{bars:0,color:`text-gray-400 dark:text-gray-500`,snr:-999,quality:`None`};let t=o(e,a.value);return c(Math.max(-30,Math.min(20,t)),u.value)},noiseFloor:a,spreadingFactor:l,minSNR:u}}export{l as t};
@@ -0,0 +1 @@
import{t as e}from"./websocket-BBUHaGQm.js";export{e as useWebSocketStore};
@@ -1 +1 @@
import{H as e,o as t}from"./runtime-core.esm-bundler-BiwffnQU.js";import{n}from"./pinia-IvTtuI9d.js";import{c as r,f as i,i as a,l as o}from"./api-DRn_D8oU.js";import{t as s}from"./packets-D3t50H8n.js";import{t as c}from"./system-DN2LtoaR.js";import{t as l}from"./dataService-C7SHAYZL.js";var u=n(`websocket`,()=>{let n=e(null),u=e(`idle`),d=e(0),f=e(Date.now()),p=e(null),m=e(null),h=e(!1),g=e(!1),_=e(!1),v=e({visible:!1,message:``,variant:`info`}),y=null,b=s(),x=c(),S=a(),C=l(),w=t(()=>u.value===`open`);function T(e,t,n=0){y!==null&&(clearTimeout(y),y=null),v.value={visible:!0,message:e,variant:t},n>0&&(y=window.setTimeout(()=>{E()},n))}function E(){y!==null&&(clearTimeout(y),y=null),v.value.visible=!1}function D(){p.value!==null&&(clearTimeout(p.value),p.value=null)}function O(){m.value!==null&&(clearInterval(m.value),m.value=null)}function k(){T(`Reconnecting...`,`info`)}function A(){let e=o();return!h.value&&!g.value&&!!e&&!i()&&S.canMaintainConnections}function j(){let e,t=o(),n=r(),i=new URLSearchParams;return t&&i.set(`token`,t),n&&i.set(`client_id`,n),e=`${window.location.protocol===`https:`?`wss:`:`ws:`}//${``?.trim()?new URL(``).host:window.location.host}/ws/packets?${i.toString()}`,e}async function M(){await C.onReconnect()}function N(e=!1){O(),n.value&&e&&(n.value.onopen=null,n.value.onmessage=null,n.value.onerror=null,n.value.onclose=null)}function P(){if(D(),!A()){u.value=`closed`;return}if(d.value>=6){u.value=`closed`,T(`Connection lost`,`error`,5e3);return}u.value=`reconnecting`,k();let e=Math.min(1e3*2**d.value,3e4);d.value+=1,p.value=window.setTimeout(()=>{p.value=null,F(!0)},e)}function F(e=!1){if(!A()||n.value?.readyState===WebSocket.OPEN||n.value?.readyState===WebSocket.CONNECTING)return;D(),N(!0),u.value=e||d.value>0||_.value?`reconnecting`:`connecting`,_.value&&k();let t=new WebSocket(j());n.value=t,t.onopen=()=>{u.value=`open`,f.value=Date.now();let e=d.value>0||_.value;d.value=0,_.value=!1,O(),m.value=window.setInterval(()=>{n.value?.readyState===WebSocket.OPEN&&(n.value.send(JSON.stringify({type:`ping`})),Date.now()-f.value>6e4&&(N(!0),n.value?.close()))},3e4),e?(C.onReconnect(),T(`Back online`,`success`,2500)):E()},t.onmessage=e=>{try{let t=JSON.parse(e.data);t.type===`packet`?b.addRealtimePacket(t.data):t.type===`stats`?(t.data?.packet_stats&&b.updateRealtimeStats({packet_stats:t.data.packet_stats}),t.data?.system_stats&&x.updateRealtimeStats(t.data.system_stats)):t.type===`packet_stats`?b.updateRealtimeStats(t.data):t.type===`system_stats`?x.updateRealtimeStats(t.data):(t.type===`pong`||t.type===`ping`)&&(f.value=Date.now(),t.type===`ping`&&n.value?.readyState===WebSocket.OPEN&&n.value.send(JSON.stringify({type:`pong`})))}catch(e){console.error(`[WebSocket] Parse error:`,e)}},t.onerror=()=>{u.value=d.value>0?`reconnecting`:`closed`},t.onclose=e=>{let t=n.value;if(N(),t===n.value&&(n.value=null),h.value||g.value){u.value=`closed`;return}if(e.code===1008||e.code===4001||e.code===4003){S.handleAuthFailure(`expired`);return}P()}}function I(e=`lifecycle`){if(g.value=!0,D(),u.value=`closed`,e===`offline`?(_.value=!0,T(`Connection lost`,`error`,4e3)):e===`hidden`?(_.value=!0,E()):e===`logout`&&(_.value=!1,E()),n.value){let e=n.value;n.value=null,N(!0),e.close()}}function L(){h.value=!1,g.value=!1}function R(e={}){h.value=e.preventReconnect??h.value,e.silent||E(),I(e.preventReconnect?`logout`:`lifecycle`),d.value=0}return{isConnected:w,connectionState:u,reconnectAttempts:d,snackbar:v,connect:F,disconnect:R,pause:I,allowReconnect:L,hideSnackbar:E,resyncData:M}});export{u as t};
import{H as e,o as t}from"./runtime-core.esm-bundler-BiwffnQU.js";import{n}from"./pinia-IvTtuI9d.js";import{c as r,f as i,i as a,l as o}from"./api-D2kWYBuR.js";import{t as s}from"./packets-BzRHEedS.js";import{t as c}from"./system--i3xLloC.js";import{t as l}from"./dataService-CmHmU2Vk.js";var u=n(`websocket`,()=>{let n=e(null),u=e(`idle`),d=e(0),f=e(Date.now()),p=e(null),m=e(null),h=e(!1),g=e(!1),_=e(!1),v=e({visible:!1,message:``,variant:`info`}),y=null,b=s(),x=c(),S=a(),C=l(),w=t(()=>u.value===`open`);function T(e,t,n=0){y!==null&&(clearTimeout(y),y=null),v.value={visible:!0,message:e,variant:t},n>0&&(y=window.setTimeout(()=>{E()},n))}function E(){y!==null&&(clearTimeout(y),y=null),v.value.visible=!1}function D(){p.value!==null&&(clearTimeout(p.value),p.value=null)}function O(){m.value!==null&&(clearInterval(m.value),m.value=null)}function k(){T(`Reconnecting...`,`info`)}function A(){let e=o();return!h.value&&!g.value&&!!e&&!i()&&S.canMaintainConnections}function j(){let e,t=o(),n=r(),i=new URLSearchParams;return t&&i.set(`token`,t),n&&i.set(`client_id`,n),e=`${window.location.protocol===`https:`?`wss:`:`ws:`}//${``?.trim()?new URL(``).host:window.location.host}/ws/packets?${i.toString()}`,e}async function M(){await C.onReconnect()}function N(e=!1){O(),n.value&&e&&(n.value.onopen=null,n.value.onmessage=null,n.value.onerror=null,n.value.onclose=null)}function P(){if(D(),!A()){u.value=`closed`;return}if(d.value>=6){u.value=`closed`,T(`Connection lost`,`error`,5e3);return}u.value=`reconnecting`,k();let e=Math.min(1e3*2**d.value,3e4);d.value+=1,p.value=window.setTimeout(()=>{p.value=null,F(!0)},e)}function F(e=!1){if(!A()||n.value?.readyState===WebSocket.OPEN||n.value?.readyState===WebSocket.CONNECTING)return;D(),N(!0),u.value=e||d.value>0||_.value?`reconnecting`:`connecting`,_.value&&k();let t=new WebSocket(j());n.value=t,t.onopen=()=>{u.value=`open`,f.value=Date.now();let e=d.value>0||_.value;d.value=0,_.value=!1,O(),m.value=window.setInterval(()=>{n.value?.readyState===WebSocket.OPEN&&(n.value.send(JSON.stringify({type:`ping`})),Date.now()-f.value>6e4&&(N(!0),n.value?.close()))},3e4),e?(C.onReconnect(),T(`Back online`,`success`,2500)):E()},t.onmessage=e=>{try{let t=JSON.parse(e.data);t.type===`packet`?b.addRealtimePacket(t.data):t.type===`stats`?(t.data?.packet_stats&&b.updateRealtimeStats({packet_stats:t.data.packet_stats}),t.data?.system_stats&&x.updateRealtimeStats(t.data.system_stats)):t.type===`packet_stats`?b.updateRealtimeStats(t.data):t.type===`system_stats`?x.updateRealtimeStats(t.data):(t.type===`pong`||t.type===`ping`)&&(f.value=Date.now(),t.type===`ping`&&n.value?.readyState===WebSocket.OPEN&&n.value.send(JSON.stringify({type:`pong`})))}catch(e){console.error(`[WebSocket] Parse error:`,e)}},t.onerror=()=>{u.value=d.value>0?`reconnecting`:`closed`},t.onclose=e=>{let t=n.value;if(N(),t===n.value&&(n.value=null),h.value||g.value){u.value=`closed`;return}if(e.code===1008||e.code===4001||e.code===4003){S.handleAuthFailure(`expired`);return}P()}}function I(e=`lifecycle`){if(g.value=!0,D(),u.value=`closed`,e===`offline`?(_.value=!0,T(`Connection lost`,`error`,4e3)):e===`hidden`?(_.value=!0,E()):e===`logout`&&(_.value=!1,E()),n.value){let e=n.value;n.value=null,N(!0),e.close()}}function L(){h.value=!1,g.value=!1}function R(e={}){h.value=e.preventReconnect??h.value,e.silent||E(),I(e.preventReconnect?`logout`:`lifecycle`),d.value=0}return{isConnected:w,connectionState:u,reconnectAttempts:d,snackbar:v,connect:F,disconnect:R,pause:I,allowReconnect:L,hideSnackbar:E,resyncData:M}});export{u as t};
@@ -1 +0,0 @@
import{t as e}from"./websocket-CiIDRmrE.js";export{e as useWebSocketStore};
+7 -7
View File
@@ -8,18 +8,18 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/assets/index-Bwi6QvPf.js"></script>
<script type="module" crossorigin src="/assets/index-T8oWhJNd.js"></script>
<link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-TcpyXLsZ.js">
<link rel="modulepreload" crossorigin href="/assets/chunk-DECur_0Z.js">
<link rel="modulepreload" crossorigin href="/assets/runtime-core.esm-bundler-BiwffnQU.js">
<link rel="modulepreload" crossorigin href="/assets/pinia-IvTtuI9d.js">
<link rel="modulepreload" crossorigin href="/assets/api-DRn_D8oU.js">
<link rel="modulepreload" crossorigin href="/assets/api-D2kWYBuR.js">
<link rel="modulepreload" crossorigin href="/assets/useTheme-CZ2Aak59.js">
<link rel="modulepreload" crossorigin href="/assets/packets-D3t50H8n.js">
<link rel="modulepreload" crossorigin href="/assets/system-DN2LtoaR.js">
<link rel="modulepreload" crossorigin href="/assets/dataService-C7SHAYZL.js">
<link rel="modulepreload" crossorigin href="/assets/websocket-CiIDRmrE.js">
<link rel="stylesheet" crossorigin href="/assets/index-CDyznwmx.css">
<link rel="modulepreload" crossorigin href="/assets/packets-BzRHEedS.js">
<link rel="modulepreload" crossorigin href="/assets/system--i3xLloC.js">
<link rel="modulepreload" crossorigin href="/assets/dataService-CmHmU2Vk.js">
<link rel="modulepreload" crossorigin href="/assets/websocket-BBUHaGQm.js">
<link rel="stylesheet" crossorigin href="/assets/index-QD4zKXF9.css">
</head>
<body>
<div id="app"></div>
+63
View File
@@ -0,0 +1,63 @@
from repeater.sensors import SensorBase, SensorConfigManager, SensorManager, SensorRegistry
class _DummySensor(SensorBase):
sensor_type = "dummy"
def _read(self):
return {"value": self.settings.get("value", 0)}
class _FailingSensor(SensorBase):
sensor_type = "failing"
def _read(self):
raise RuntimeError("boom")
SensorRegistry.register("dummy", _DummySensor)
SensorRegistry.register("failing", _FailingSensor)
def test_sensor_config_manager_reads_sensor_definitions():
config = {
"sensors": {
"enabled": True,
"poll_interval_seconds": 12,
"definitions": [
{"name": "demo", "type": "dummy", "settings": {"value": 7}},
],
}
}
manager = SensorConfigManager(config)
assert manager.is_enabled() is True
assert manager.get_poll_interval_seconds() == 12.0
assert manager.get_definitions()[0]["type"] == "dummy"
def test_sensor_manager_loads_and_reads_sensors_without_stopping_on_failure():
config = {
"sensors": {
"enabled": True,
"definitions": [
{"name": "good", "type": "dummy", "settings": {"value": 11}},
{"name": "bad", "type": "failing"},
{"name": "skipped", "type": "missing", "enabled": True},
],
}
}
manager = SensorManager(config)
summary = manager.get_summary()
assert summary["configured"] == 3
assert summary["loaded"] == 2
readings = manager.read_all()
assert len(readings) == 2
assert readings[0]["ok"] is True
assert readings[0]["data"]["value"] == 11
assert readings[1]["ok"] is False
assert readings[1]["error"].startswith("RuntimeError:")