ws: migrate push stream to data streaming

- daemon: send full snapshot on connect; versions/tick now carry the
  full state of one subsystem (subsystem + data); no legacy
  updated/subsystems payloads; refresh_state and POST /status/refresh
  broadcast per-subsystem versions with data
- client: modelSet() patches models in place; onMessage/topic refresh
  retired; 3s initial-load fallback via new POST /api/status/refresh
- schema: lib/schema.py TypedDicts + hoover/schema.js defaults +
  docs/state-model.md as single source of truth for state shapes
- system: poll at 1s, volatile metrics registered, dashboard uses a
  dedicated system model (status model removed)
- firewall: refuse to strip both https and ssh from the default zone
  (409, force override via UI confirm); set_zone_services persists
  services to the declarative config; collector exposes default_zone
- UI: pages migrate to flat state shapes; post-mutation modelFetch
  refreshes removed (WS delta covers it)
- tests: ws snapshot/delta/broadcast, refresh-state, schema types,
  model-set/js ws handler and reconnect fallback
This commit is contained in:
2026-08-20 01:38:00 +00:00
parent 9c9f92ad04
commit 332d14e37d
45 changed files with 2819 additions and 496 deletions
+57
View File
@@ -40,6 +40,11 @@ def _ne(func, **kw):
return _patch(f"webui.api.network.{func}", **kw)
def _st(func, **kw):
"""Patch daemon.client.{func} in the status blueprint namespace."""
return _patch(f"webui.api.status.{func}", **kw)
@pytest.fixture
def client():
from flask import Flask
@@ -883,3 +888,55 @@ class TestNetworkApplyAll:
mock_post.side_effect = RuntimeError("apply failed")
resp = client.post("/api/network/apply")
assert resp.status_code == 500
# ============================================================================
# Status
# ============================================================================
@pytest.fixture
def status_client():
from flask import Flask
from webui.api.status import bp as status_bp
app = Flask(__name__)
app.register_blueprint(status_bp, url_prefix="/api/status")
return app.test_client()
class TestStatusRefresh:
def test_filtered_subsystems_passed_through(self, status_client):
"""The subsystem body is forwarded to the daemon POST endpoint."""
from daemon.iface import POST_STATUS_REFRESH
with _st("post") as mock_post:
mock_post.return_value = {"firewall": {"zones": {}}}
resp = status_client.post(
"/api/status/refresh", json={"subsystems": ["firewall"]}
)
assert resp.status_code == 200
data = resp.get_json()
assert data["ok"] is True
assert data["data"] == {"firewall": {"zones": {}}}
mock_post.assert_called_once_with(
POST_STATUS_REFRESH, {"subsystems": ["firewall"]}
)
def test_empty_body_forwards_empty_dict(self, status_client):
"""An empty body becomes {} (daemon-side 'all subsystems' default)."""
from daemon.iface import POST_STATUS_REFRESH
with _st("post") as mock_post:
mock_post.return_value = {}
resp = status_client.post("/api/status/refresh")
assert resp.status_code == 200
mock_post.assert_called_once_with(POST_STATUS_REFRESH, {})
@_st("post")
def test_runtime_error(self, mock_post, status_client):
mock_post.side_effect = RuntimeError("no daemon")
resp = status_client.post("/api/status/refresh", json={})
assert resp.status_code == 500
assert resp.get_json()["ok"] is False