diff --git a/freecad/silo_commands.py b/freecad/silo_commands.py index 01ba57f..9df5c2e 100644 --- a/freecad/silo_commands.py +++ b/freecad/silo_commands.py @@ -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()