feat: reflect server mode in client UI
Add server mode awareness to the FreeCAD client. The mode (normal, read-only, degraded, offline) is fetched from the /ready endpoint on each status refresh and updated in real-time via SSE server.state events. Changes: - Add _server_mode global and _fetch_server_mode() helper that queries /ready and maps response status to mode string - Add server_mode_changed signal to SiloEventListener, handle server.state SSE events in _dispatch() - Add mode banner to SiloAuthDockWidget: colored bar shown when server is not in normal mode (yellow=read-only, orange=degraded, red=offline) - Update _refresh_status() to fetch mode and update banner - Disable write commands (New, Save, Commit, Push) when server is not in normal mode via IsActive() checks Closes #4
This commit is contained in:
@@ -143,6 +143,39 @@ def _clear_auth():
|
||||
_fc_settings.clear_auth()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Server mode tracking
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_server_mode = "offline" # "normal" | "read-only" | "degraded" | "offline"
|
||||
|
||||
|
||||
def _fetch_server_mode() -> str:
|
||||
"""Fetch server mode from the /ready endpoint.
|
||||
|
||||
Returns one of: "normal", "read-only", "degraded", "offline".
|
||||
"""
|
||||
api_url = _get_api_url().rstrip("/")
|
||||
base_url = api_url[:-4] if api_url.endswith("/api") else api_url
|
||||
url = f"{base_url}/ready"
|
||||
try:
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
resp = urllib.request.urlopen(req, context=_get_ssl_context(), timeout=10)
|
||||
body = resp.read(4096).decode("utf-8", errors="replace")
|
||||
data = json.loads(body)
|
||||
status = data.get("status", "")
|
||||
if status in ("ok", "ready"):
|
||||
return "normal"
|
||||
if status in ("read-only", "read_only", "readonly"):
|
||||
return "read-only"
|
||||
if status in ("degraded",):
|
||||
return "degraded"
|
||||
# Unknown status but server responded — treat as normal
|
||||
return "normal"
|
||||
except Exception:
|
||||
return "offline"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Icon helper
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -813,7 +846,7 @@ class Silo_New:
|
||||
QtGui.QMessageBox.critical(None, "Error", str(e))
|
||||
|
||||
def IsActive(self):
|
||||
return True
|
||||
return _server_mode == "normal"
|
||||
|
||||
|
||||
class Silo_Save:
|
||||
@@ -920,7 +953,7 @@ class Silo_Save:
|
||||
FreeCAD.Console.PrintMessage("File saved locally but not uploaded.\n")
|
||||
|
||||
def IsActive(self):
|
||||
return FreeCAD.ActiveDocument is not None
|
||||
return FreeCAD.ActiveDocument is not None and _server_mode == "normal"
|
||||
|
||||
|
||||
class Silo_Commit:
|
||||
@@ -973,7 +1006,7 @@ class Silo_Commit:
|
||||
FreeCAD.Console.PrintError(f"Commit failed: {e}\n")
|
||||
|
||||
def IsActive(self):
|
||||
return FreeCAD.ActiveDocument is not None
|
||||
return FreeCAD.ActiveDocument is not None and _server_mode == "normal"
|
||||
|
||||
|
||||
def _check_pull_conflicts(part_number, local_path, doc=None):
|
||||
@@ -1345,7 +1378,7 @@ class Silo_Push:
|
||||
QtGui.QMessageBox.information(None, "Push", f"Uploaded {uploaded} files.")
|
||||
|
||||
def IsActive(self):
|
||||
return True
|
||||
return _server_mode == "normal"
|
||||
|
||||
|
||||
class Silo_Info:
|
||||
@@ -2438,6 +2471,7 @@ class SiloEventListener(QtCore.QThread):
|
||||
connection_status = QtCore.Signal(
|
||||
str
|
||||
) # "connected" / "disconnected" / "unsupported"
|
||||
server_mode_changed = QtCore.Signal(str) # "normal" / "read-only" / "degraded"
|
||||
|
||||
_MAX_FAST_RETRIES = 3
|
||||
_FAST_RETRY_SECS = 5
|
||||
@@ -2538,6 +2572,12 @@ class SiloEventListener(QtCore.QThread):
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
return
|
||||
|
||||
if event_type == "server.state":
|
||||
mode = payload.get("mode", "")
|
||||
if mode:
|
||||
self.server_mode_changed.emit(mode)
|
||||
return
|
||||
|
||||
pn = payload.get("part_number", "")
|
||||
if not pn:
|
||||
return
|
||||
@@ -2645,6 +2685,13 @@ class SiloAuthDockWidget:
|
||||
sse_row.addStretch()
|
||||
layout.addLayout(sse_row)
|
||||
|
||||
# Server mode banner (hidden when normal)
|
||||
self._mode_banner = QtGui.QLabel("")
|
||||
self._mode_banner.setWordWrap(True)
|
||||
self._mode_banner.setContentsMargins(6, 4, 6, 4)
|
||||
self._mode_banner.setVisible(False)
|
||||
layout.addWidget(self._mode_banner)
|
||||
|
||||
layout.addSpacing(4)
|
||||
|
||||
# Buttons
|
||||
@@ -2734,6 +2781,14 @@ class SiloAuthDockWidget:
|
||||
self._login_btn.setText("Login")
|
||||
self._login_btn.clicked.connect(self._on_login_clicked)
|
||||
|
||||
# Fetch and display server mode
|
||||
global _server_mode
|
||||
if reachable:
|
||||
_server_mode = _fetch_server_mode()
|
||||
else:
|
||||
_server_mode = "offline"
|
||||
self._update_mode_banner()
|
||||
|
||||
# Manage SSE listener based on auth state
|
||||
self._sync_event_listener(authed)
|
||||
|
||||
@@ -2747,6 +2802,7 @@ class SiloAuthDockWidget:
|
||||
self._event_listener.item_updated.connect(self._on_remote_change)
|
||||
self._event_listener.revision_created.connect(self._on_remote_revision)
|
||||
self._event_listener.connection_status.connect(self._on_sse_status)
|
||||
self._event_listener.server_mode_changed.connect(self._on_server_mode)
|
||||
self._event_listener.start()
|
||||
else:
|
||||
if self._event_listener is not None and self._event_listener.isRunning():
|
||||
@@ -2764,6 +2820,35 @@ class SiloAuthDockWidget:
|
||||
self._sse_label.setText("Not available")
|
||||
self._sse_label.setStyleSheet("font-size: 11px; color: #888;")
|
||||
|
||||
def _on_server_mode(self, mode):
|
||||
global _server_mode
|
||||
_server_mode = mode
|
||||
self._update_mode_banner()
|
||||
|
||||
def _update_mode_banner(self):
|
||||
_MODE_BANNERS = {
|
||||
"normal": ("", "", False),
|
||||
"read-only": (
|
||||
"Server is in read-only mode",
|
||||
"background: #FFC107; color: #000; padding: 4px; font-size: 11px;",
|
||||
True,
|
||||
),
|
||||
"degraded": (
|
||||
"MinIO unavailable \u2014 file ops limited",
|
||||
"background: #FF9800; color: #000; padding: 4px; font-size: 11px;",
|
||||
True,
|
||||
),
|
||||
"offline": (
|
||||
"Disconnected from silo",
|
||||
"background: #F44336; color: #fff; padding: 4px; font-size: 11px;",
|
||||
True,
|
||||
),
|
||||
}
|
||||
text, style, visible = _MODE_BANNERS.get(_server_mode, _MODE_BANNERS["offline"])
|
||||
self._mode_banner.setText(text)
|
||||
self._mode_banner.setStyleSheet(style)
|
||||
self._mode_banner.setVisible(visible)
|
||||
|
||||
def _on_remote_change(self, part_number):
|
||||
FreeCAD.Console.PrintMessage(f"Silo: Part {part_number} updated on server\n")
|
||||
mw = FreeCADGui.getMainWindow()
|
||||
|
||||
Reference in New Issue
Block a user