Compare commits
6 Commits
dca6380199
...
edbaf65923
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edbaf65923 | ||
| 80f8ec27a0 | |||
|
|
6b3e8b7518 | ||
| b3fe98c696 | |||
|
|
c537e2f08f | ||
| 29b1f32fd9 |
@@ -20,7 +20,9 @@ from silo_client import SiloClient, SiloSettings
|
||||
_PREF_GROUP = "User parameter:BaseApp/Preferences/Mod/KindredSilo"
|
||||
|
||||
# Configuration - preferences take priority over env vars
|
||||
SILO_PROJECTS_DIR = os.environ.get("SILO_PROJECTS_DIR", os.path.expanduser("~/projects"))
|
||||
SILO_PROJECTS_DIR = os.environ.get(
|
||||
"SILO_PROJECTS_DIR", os.path.expanduser("~/projects")
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -98,7 +100,9 @@ class FreeCADSiloSettings(SiloSettings):
|
||||
param = FreeCAD.ParamGet(_PREF_GROUP)
|
||||
return param.GetString("SslCertPath", "")
|
||||
|
||||
def save_auth(self, username: str, role: str = "", source: str = "", token: str = ""):
|
||||
def save_auth(
|
||||
self, username: str, role: str = "", source: str = "", token: str = ""
|
||||
):
|
||||
param = FreeCAD.ParamGet(_PREF_GROUP)
|
||||
param.SetString("AuthUsername", username)
|
||||
param.SetString("AuthRole", role)
|
||||
@@ -167,7 +171,9 @@ def _get_ssl_verify() -> bool:
|
||||
def _get_ssl_context():
|
||||
from silo_client._ssl import build_ssl_context
|
||||
|
||||
return build_ssl_context(_fc_settings.get_ssl_verify(), _fc_settings.get_ssl_cert_path())
|
||||
return build_ssl_context(
|
||||
_fc_settings.get_ssl_verify(), _fc_settings.get_ssl_cert_path()
|
||||
)
|
||||
|
||||
|
||||
def _get_auth_headers() -> Dict[str, str]:
|
||||
@@ -191,13 +197,13 @@ def _clear_auth():
|
||||
# Server mode tracking
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_server_mode = "offline" # "normal" | "read-only" | "degraded" | "offline"
|
||||
_server_mode = "offline" # "normal" | "read-only" | "offline"
|
||||
|
||||
|
||||
def _fetch_server_mode() -> str:
|
||||
"""Fetch server mode from the /ready endpoint.
|
||||
|
||||
Returns one of: "normal", "read-only", "degraded", "offline".
|
||||
Returns one of: "normal", "read-only", "offline".
|
||||
"""
|
||||
api_url = _get_api_url().rstrip("/")
|
||||
base_url = api_url[:-4] if api_url.endswith("/api") else api_url
|
||||
@@ -212,8 +218,6 @@ def _fetch_server_mode() -> str:
|
||||
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:
|
||||
@@ -224,7 +228,9 @@ def _fetch_server_mode() -> str:
|
||||
# Icon helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_ICON_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources", "icons")
|
||||
_ICON_DIR = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "resources", "icons"
|
||||
)
|
||||
|
||||
|
||||
def _icon(name):
|
||||
@@ -562,7 +568,7 @@ class SiloSync:
|
||||
def upload_file(
|
||||
self, part_number: str, file_path: str, comment: str = "Auto-save"
|
||||
) -> Optional[Dict]:
|
||||
"""Upload file to MinIO."""
|
||||
"""Upload file to the server."""
|
||||
try:
|
||||
doc = FreeCAD.openDocument(file_path)
|
||||
if not doc:
|
||||
@@ -576,7 +582,7 @@ class SiloSync:
|
||||
return None
|
||||
|
||||
def download_file(self, part_number: str) -> Optional[Path]:
|
||||
"""Download latest file from MinIO."""
|
||||
"""Download the latest revision file from the server."""
|
||||
try:
|
||||
item = self.client.get_item(part_number)
|
||||
file_path = get_cad_file_path(part_number, item.get("description", ""))
|
||||
@@ -623,7 +629,9 @@ def handle_kindred_url(url: str):
|
||||
parts = [parsed.netloc] + [p for p in parsed.path.split("/") if p]
|
||||
if len(parts) >= 2 and parts[0] == "item":
|
||||
part_number = parts[1]
|
||||
FreeCAD.Console.PrintMessage(f"Silo: Opening item {part_number} from kindred:// URL\n")
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Silo: Opening item {part_number} from kindred:// URL\n"
|
||||
)
|
||||
_sync.open_item(part_number)
|
||||
|
||||
|
||||
@@ -730,7 +738,9 @@ class Silo_New:
|
||||
},
|
||||
)
|
||||
obj.Label = part_number
|
||||
_sync.save_to_canonical_path(FreeCAD.ActiveDocument, force_rename=True)
|
||||
_sync.save_to_canonical_path(
|
||||
FreeCAD.ActiveDocument, force_rename=True
|
||||
)
|
||||
else:
|
||||
_sync.create_document_for_item(result, save=True)
|
||||
|
||||
@@ -763,18 +773,20 @@ def _push_dag_after_upload(doc, part_number, revision_number):
|
||||
result = _client.push_dag(part_number, revision_number, nodes, edges)
|
||||
node_count = result.get("node_count", len(nodes))
|
||||
edge_count = result.get("edge_count", len(edges))
|
||||
FreeCAD.Console.PrintMessage(f"DAG synced: {node_count} nodes, {edge_count} edges\n")
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"DAG synced: {node_count} nodes, {edge_count} edges\n"
|
||||
)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintWarning(f"DAG sync failed: {e}\n")
|
||||
|
||||
|
||||
class Silo_Save:
|
||||
"""Save locally and upload to MinIO."""
|
||||
"""Save locally and upload to the server."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"MenuText": "Save",
|
||||
"ToolTip": "Save locally and upload to MinIO (Ctrl+S)",
|
||||
"ToolTip": "Save locally and upload to server (Ctrl+S)",
|
||||
"Pixmap": _icon("save"),
|
||||
}
|
||||
|
||||
@@ -829,9 +841,11 @@ class Silo_Save:
|
||||
|
||||
FreeCAD.Console.PrintMessage(f"Saved: {file_path}\n")
|
||||
|
||||
# Try to upload to MinIO
|
||||
# Try to upload to server
|
||||
try:
|
||||
result = _client._upload_file(part_number, str(file_path), properties, "Auto-save")
|
||||
result = _client._upload_file(
|
||||
part_number, str(file_path), properties, "Auto-save"
|
||||
)
|
||||
|
||||
new_rev = result["revision_number"]
|
||||
FreeCAD.Console.PrintMessage(f"Uploaded as revision {new_rev}\n")
|
||||
@@ -866,7 +880,9 @@ class Silo_Commit:
|
||||
|
||||
obj = get_tracked_object(doc)
|
||||
if not obj:
|
||||
FreeCAD.Console.PrintError("No tracked object. Use 'New' to register first.\n")
|
||||
FreeCAD.Console.PrintError(
|
||||
"No tracked object. Use 'New' to register first.\n"
|
||||
)
|
||||
return
|
||||
|
||||
part_number = obj.SiloPartNumber
|
||||
@@ -883,7 +899,9 @@ class Silo_Commit:
|
||||
if not file_path:
|
||||
return
|
||||
|
||||
result = _client._upload_file(part_number, str(file_path), properties, comment)
|
||||
result = _client._upload_file(
|
||||
part_number, str(file_path), properties, comment
|
||||
)
|
||||
|
||||
new_rev = result["revision_number"]
|
||||
FreeCAD.Console.PrintMessage(f"Committed revision {new_rev}: {comment}\n")
|
||||
@@ -934,7 +952,9 @@ def _check_pull_conflicts(part_number, local_path, doc=None):
|
||||
server_updated = item.get("updated_at", "")
|
||||
if server_updated:
|
||||
# Parse ISO format timestamp
|
||||
server_dt = datetime.datetime.fromisoformat(server_updated.replace("Z", "+00:00"))
|
||||
server_dt = datetime.datetime.fromisoformat(
|
||||
server_updated.replace("Z", "+00:00")
|
||||
)
|
||||
if server_dt > local_mtime:
|
||||
conflicts.append("Server version is newer than local file.")
|
||||
except Exception:
|
||||
@@ -964,7 +984,9 @@ class SiloPullDialog:
|
||||
# Revision table
|
||||
self._table = QtGui.QTableWidget()
|
||||
self._table.setColumnCount(5)
|
||||
self._table.setHorizontalHeaderLabels(["Rev", "Date", "Comment", "Status", "File"])
|
||||
self._table.setHorizontalHeaderLabels(
|
||||
["Rev", "Date", "Comment", "Status", "File"]
|
||||
)
|
||||
self._table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
|
||||
self._table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
|
||||
self._table.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
|
||||
@@ -1057,7 +1079,9 @@ def _pull_dependencies(part_number, progress_callback=None):
|
||||
# Skip if already exists locally
|
||||
existing = find_file_by_part_number(child_pn)
|
||||
if existing and existing.exists():
|
||||
FreeCAD.Console.PrintMessage(f" {child_pn}: already exists at {existing}\n")
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f" {child_pn}: already exists at {existing}\n"
|
||||
)
|
||||
# Still recurse — this child may itself be an assembly with missing deps
|
||||
_pull_dependencies(child_pn, progress_callback)
|
||||
continue
|
||||
@@ -1096,12 +1120,12 @@ def _pull_dependencies(part_number, progress_callback=None):
|
||||
|
||||
|
||||
class Silo_Pull:
|
||||
"""Download from MinIO / sync from database."""
|
||||
"""Download revision file from the server."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"MenuText": "Pull",
|
||||
"ToolTip": "Download from MinIO with revision selection",
|
||||
"ToolTip": "Download file with revision selection",
|
||||
"Pixmap": _icon("pull"),
|
||||
}
|
||||
|
||||
@@ -1137,14 +1161,18 @@ class Silo_Pull:
|
||||
|
||||
if not has_any_file:
|
||||
if existing_local:
|
||||
FreeCAD.Console.PrintMessage(f"Opening existing local file: {existing_local}\n")
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Opening existing local file: {existing_local}\n"
|
||||
)
|
||||
FreeCAD.openDocument(str(existing_local))
|
||||
else:
|
||||
try:
|
||||
item = _client.get_item(part_number)
|
||||
new_doc = _sync.create_document_for_item(item, save=True)
|
||||
if new_doc:
|
||||
FreeCAD.Console.PrintMessage(f"Created local file for {part_number}\n")
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Created local file for {part_number}\n"
|
||||
)
|
||||
else:
|
||||
QtGui.QMessageBox.warning(
|
||||
None,
|
||||
@@ -1231,7 +1259,9 @@ class Silo_Pull:
|
||||
progress.setValue(100)
|
||||
progress.close()
|
||||
if dep_pulled:
|
||||
FreeCAD.Console.PrintMessage(f"Pulled {len(dep_pulled)} dependency file(s)\n")
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Pulled {len(dep_pulled)} dependency file(s)\n"
|
||||
)
|
||||
|
||||
# Close existing document if open, then reopen
|
||||
if doc and doc.FileName == str(dest_path):
|
||||
@@ -1251,12 +1281,12 @@ class Silo_Pull:
|
||||
|
||||
|
||||
class Silo_Push:
|
||||
"""Upload local files to MinIO."""
|
||||
"""Upload local files to the server."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"MenuText": "Push",
|
||||
"ToolTip": "Upload local files that aren't in MinIO",
|
||||
"ToolTip": "Upload local files that aren't on the server",
|
||||
"Pixmap": _icon("push"),
|
||||
}
|
||||
|
||||
@@ -1286,7 +1316,9 @@ class Silo_Push:
|
||||
server_dt = datetime.fromisoformat(
|
||||
server_time_str.replace("Z", "+00:00")
|
||||
)
|
||||
local_dt = datetime.fromtimestamp(local_mtime, tz=timezone.utc)
|
||||
local_dt = datetime.fromtimestamp(
|
||||
local_mtime, tz=timezone.utc
|
||||
)
|
||||
if local_dt > server_dt:
|
||||
unuploaded.append(lf)
|
||||
else:
|
||||
@@ -1299,7 +1331,9 @@ class Silo_Push:
|
||||
pass # Not in DB, skip
|
||||
|
||||
if not unuploaded:
|
||||
QtGui.QMessageBox.information(None, "Push", "All local files are already uploaded.")
|
||||
QtGui.QMessageBox.information(
|
||||
None, "Push", "All local files are already uploaded."
|
||||
)
|
||||
return
|
||||
|
||||
msg = f"Found {len(unuploaded)} files to upload:\n\n"
|
||||
@@ -1317,7 +1351,9 @@ class Silo_Push:
|
||||
|
||||
uploaded = 0
|
||||
for item in unuploaded:
|
||||
result = _sync.upload_file(item["part_number"], item["path"], "Synced from local")
|
||||
result = _sync.upload_file(
|
||||
item["part_number"], item["path"], "Synced from local"
|
||||
)
|
||||
if result:
|
||||
uploaded += 1
|
||||
|
||||
@@ -1366,14 +1402,12 @@ class Silo_Info:
|
||||
msg = f"<h3>{part_number}</h3>"
|
||||
msg += f"<p><b>Type:</b> {item.get('item_type', '-')}</p>"
|
||||
msg += f"<p><b>Description:</b> {item.get('description', '-')}</p>"
|
||||
msg += (
|
||||
f"<p><b>Projects:</b> {', '.join(project_codes) if project_codes else 'None'}</p>"
|
||||
)
|
||||
msg += f"<p><b>Projects:</b> {', '.join(project_codes) if project_codes else 'None'}</p>"
|
||||
msg += f"<p><b>Current Revision:</b> {item.get('current_revision', 1)}</p>"
|
||||
msg += f"<p><b>Local Revision:</b> {getattr(obj, 'SiloRevision', '-')}</p>"
|
||||
|
||||
has_file, _ = _client.has_file(part_number)
|
||||
msg += f"<p><b>File in MinIO:</b> {'Yes' if has_file else 'No'}</p>"
|
||||
msg += f"<p><b>File on Server:</b> {'Yes' if has_file else 'No'}</p>"
|
||||
|
||||
# Show current revision status
|
||||
if revisions:
|
||||
@@ -1434,7 +1468,9 @@ class Silo_TagProjects:
|
||||
try:
|
||||
# Get current projects for item
|
||||
current_projects = _client.get_item_projects(part_number)
|
||||
current_codes = {p.get("code", "") for p in current_projects if p.get("code")}
|
||||
current_codes = {
|
||||
p.get("code", "") for p in current_projects if p.get("code")
|
||||
}
|
||||
|
||||
# Get all available projects
|
||||
all_projects = _client.get_projects()
|
||||
@@ -1545,7 +1581,9 @@ class Silo_Rollback:
|
||||
dialog.setMinimumHeight(300)
|
||||
layout = QtGui.QVBoxLayout(dialog)
|
||||
|
||||
label = QtGui.QLabel(f"Select a revision to rollback to (current: Rev {current_rev}):")
|
||||
label = QtGui.QLabel(
|
||||
f"Select a revision to rollback to (current: Rev {current_rev}):"
|
||||
)
|
||||
layout.addWidget(label)
|
||||
|
||||
# Revision table
|
||||
@@ -1560,8 +1598,12 @@ class Silo_Rollback:
|
||||
for i, rev in enumerate(prev_revisions):
|
||||
table.setItem(i, 0, QtGui.QTableWidgetItem(str(rev["revision_number"])))
|
||||
table.setItem(i, 1, QtGui.QTableWidgetItem(rev.get("status", "draft")))
|
||||
table.setItem(i, 2, QtGui.QTableWidgetItem(rev.get("created_at", "")[:10]))
|
||||
table.setItem(i, 3, QtGui.QTableWidgetItem(rev.get("comment", "") or ""))
|
||||
table.setItem(
|
||||
i, 2, QtGui.QTableWidgetItem(rev.get("created_at", "")[:10])
|
||||
)
|
||||
table.setItem(
|
||||
i, 3, QtGui.QTableWidgetItem(rev.get("comment", "") or "")
|
||||
)
|
||||
|
||||
table.resizeColumnsToContents()
|
||||
layout.addWidget(table)
|
||||
@@ -1587,7 +1629,9 @@ class Silo_Rollback:
|
||||
def on_rollback():
|
||||
selected = table.selectedItems()
|
||||
if not selected:
|
||||
QtGui.QMessageBox.warning(dialog, "Rollback", "Please select a revision")
|
||||
QtGui.QMessageBox.warning(
|
||||
dialog, "Rollback", "Please select a revision"
|
||||
)
|
||||
return
|
||||
selected_rev[0] = int(table.item(selected[0].row(), 0).text())
|
||||
dialog.accept()
|
||||
@@ -1685,7 +1729,9 @@ class Silo_SetStatus:
|
||||
# Update status
|
||||
_client.update_revision(part_number, rev_num, status=status)
|
||||
|
||||
FreeCAD.Console.PrintMessage(f"Updated Rev {rev_num} status to '{status}'\n")
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Updated Rev {rev_num} status to '{status}'\n"
|
||||
)
|
||||
QtGui.QMessageBox.information(
|
||||
None, "Status Updated", f"Revision {rev_num} status set to '{status}'"
|
||||
)
|
||||
@@ -1774,7 +1820,9 @@ class Silo_Settings:
|
||||
ssl_checkbox.setChecked(param.GetBool("SslVerify", True))
|
||||
layout.addWidget(ssl_checkbox)
|
||||
|
||||
ssl_hint = QtGui.QLabel("Disable only for internal servers with self-signed certificates.")
|
||||
ssl_hint = QtGui.QLabel(
|
||||
"Disable only for internal servers with self-signed certificates."
|
||||
)
|
||||
ssl_hint.setWordWrap(True)
|
||||
ssl_hint.setStyleSheet("color: #888; font-size: 11px;")
|
||||
layout.addWidget(ssl_hint)
|
||||
@@ -2053,7 +2101,9 @@ class Silo_BOM:
|
||||
|
||||
wu_table = QtGui.QTableWidget()
|
||||
wu_table.setColumnCount(5)
|
||||
wu_table.setHorizontalHeaderLabels(["Parent Part Number", "Type", "Qty", "Unit", "Ref Des"])
|
||||
wu_table.setHorizontalHeaderLabels(
|
||||
["Parent Part Number", "Type", "Qty", "Unit", "Ref Des"]
|
||||
)
|
||||
wu_table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
|
||||
wu_table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
|
||||
wu_table.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
|
||||
@@ -2082,12 +2132,16 @@ class Silo_BOM:
|
||||
bom_table.setItem(
|
||||
row, 1, QtGui.QTableWidgetItem(entry.get("child_description", ""))
|
||||
)
|
||||
bom_table.setItem(row, 2, QtGui.QTableWidgetItem(entry.get("rel_type", "")))
|
||||
bom_table.setItem(
|
||||
row, 2, QtGui.QTableWidgetItem(entry.get("rel_type", ""))
|
||||
)
|
||||
qty = entry.get("quantity")
|
||||
bom_table.setItem(
|
||||
row, 3, QtGui.QTableWidgetItem(str(qty) if qty is not None else "")
|
||||
)
|
||||
bom_table.setItem(row, 4, QtGui.QTableWidgetItem(entry.get("unit") or ""))
|
||||
bom_table.setItem(
|
||||
row, 4, QtGui.QTableWidgetItem(entry.get("unit") or "")
|
||||
)
|
||||
ref_des = entry.get("reference_designators") or []
|
||||
bom_table.setItem(row, 5, QtGui.QTableWidgetItem(", ".join(ref_des)))
|
||||
bom_table.setItem(
|
||||
@@ -2109,12 +2163,16 @@ class Silo_BOM:
|
||||
wu_table.setItem(
|
||||
row, 0, QtGui.QTableWidgetItem(entry.get("parent_part_number", ""))
|
||||
)
|
||||
wu_table.setItem(row, 1, QtGui.QTableWidgetItem(entry.get("rel_type", "")))
|
||||
wu_table.setItem(
|
||||
row, 1, QtGui.QTableWidgetItem(entry.get("rel_type", ""))
|
||||
)
|
||||
qty = entry.get("quantity")
|
||||
wu_table.setItem(
|
||||
row, 2, QtGui.QTableWidgetItem(str(qty) if qty is not None else "")
|
||||
)
|
||||
wu_table.setItem(row, 3, QtGui.QTableWidgetItem(entry.get("unit") or ""))
|
||||
wu_table.setItem(
|
||||
row, 3, QtGui.QTableWidgetItem(entry.get("unit") or "")
|
||||
)
|
||||
ref_des = entry.get("reference_designators") or []
|
||||
wu_table.setItem(row, 4, QtGui.QTableWidgetItem(", ".join(ref_des)))
|
||||
wu_table.resizeColumnsToContents()
|
||||
@@ -2167,7 +2225,9 @@ class Silo_BOM:
|
||||
try:
|
||||
qty = float(qty_text)
|
||||
except ValueError:
|
||||
QtGui.QMessageBox.warning(dialog, "BOM", "Quantity must be a number.")
|
||||
QtGui.QMessageBox.warning(
|
||||
dialog, "BOM", "Quantity must be a number."
|
||||
)
|
||||
return
|
||||
|
||||
unit = unit_input.text().strip() or None
|
||||
@@ -2246,7 +2306,9 @@ class Silo_BOM:
|
||||
try:
|
||||
new_qty = float(qty_text)
|
||||
except ValueError:
|
||||
QtGui.QMessageBox.warning(dialog, "BOM", "Quantity must be a number.")
|
||||
QtGui.QMessageBox.warning(
|
||||
dialog, "BOM", "Quantity must be a number."
|
||||
)
|
||||
return
|
||||
|
||||
new_unit = unit_input.text().strip() or None
|
||||
@@ -2270,7 +2332,9 @@ class Silo_BOM:
|
||||
)
|
||||
load_bom()
|
||||
except Exception as exc:
|
||||
QtGui.QMessageBox.warning(dialog, "BOM", f"Failed to update entry:\n{exc}")
|
||||
QtGui.QMessageBox.warning(
|
||||
dialog, "BOM", f"Failed to update entry:\n{exc}"
|
||||
)
|
||||
|
||||
def on_remove():
|
||||
selected = bom_table.selectedItems()
|
||||
@@ -2296,7 +2360,9 @@ class Silo_BOM:
|
||||
_client.delete_bom_entry(part_number, child_pn)
|
||||
load_bom()
|
||||
except Exception as exc:
|
||||
QtGui.QMessageBox.warning(dialog, "BOM", f"Failed to remove entry:\n{exc}")
|
||||
QtGui.QMessageBox.warning(
|
||||
dialog, "BOM", f"Failed to remove entry:\n{exc}"
|
||||
)
|
||||
|
||||
add_btn.clicked.connect(on_add)
|
||||
edit_btn.clicked.connect(on_edit)
|
||||
@@ -2335,8 +2401,10 @@ class SiloEventListener(QtCore.QThread):
|
||||
|
||||
item_updated = QtCore.Signal(str) # part_number
|
||||
revision_created = QtCore.Signal(str, int) # part_number, revision
|
||||
connection_status = QtCore.Signal(str, int, str) # (status, retry_count, error_message)
|
||||
server_mode_changed = QtCore.Signal(str) # "normal" / "read-only" / "degraded"
|
||||
connection_status = QtCore.Signal(
|
||||
str, int, str
|
||||
) # (status, retry_count, error_message)
|
||||
server_mode_changed = QtCore.Signal(str) # "normal" / "read-only" / "offline"
|
||||
|
||||
# DAG events
|
||||
dag_updated = QtCore.Signal(str, int, int) # part_number, node_count, edge_count
|
||||
@@ -2421,7 +2489,9 @@ class SiloEventListener(QtCore.QThread):
|
||||
req = urllib.request.Request(url, headers=headers, method="GET")
|
||||
|
||||
try:
|
||||
self._response = urllib.request.urlopen(req, context=_get_ssl_context(), timeout=90)
|
||||
self._response = urllib.request.urlopen(
|
||||
req, context=_get_ssl_context(), timeout=90
|
||||
)
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code in (404, 501):
|
||||
raise _SSEUnsupported()
|
||||
@@ -2647,7 +2717,9 @@ class SiloAuthDockWidget:
|
||||
layout.addLayout(btn_row)
|
||||
|
||||
# Keep the auth panel compact so the Activity panel below gets more space
|
||||
self.widget.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Maximum)
|
||||
self.widget.setSizePolicy(
|
||||
QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Maximum
|
||||
)
|
||||
|
||||
# -- Status refresh -----------------------------------------------------
|
||||
|
||||
@@ -2761,7 +2833,9 @@ class SiloAuthDockWidget:
|
||||
FreeCAD.Console.PrintMessage("Silo: SSE connected\n")
|
||||
self._seed_activity_feed()
|
||||
elif status == "disconnected":
|
||||
self._sse_label.setText(f"Reconnecting ({retry}/{SiloEventListener._MAX_RETRIES})...")
|
||||
self._sse_label.setText(
|
||||
f"Reconnecting ({retry}/{SiloEventListener._MAX_RETRIES})..."
|
||||
)
|
||||
self._sse_label.setStyleSheet("font-size: 11px; color: #FF9800;")
|
||||
self._sse_label.setToolTip(error or "Connection lost")
|
||||
FreeCAD.Console.PrintWarning(
|
||||
@@ -2771,7 +2845,9 @@ class SiloAuthDockWidget:
|
||||
self._sse_label.setText("Disconnected")
|
||||
self._sse_label.setStyleSheet("font-size: 11px; color: #F44336;")
|
||||
self._sse_label.setToolTip(error or "Max retries reached")
|
||||
FreeCAD.Console.PrintError(f"Silo: SSE gave up after {retry} retries: {error}\n")
|
||||
FreeCAD.Console.PrintError(
|
||||
f"Silo: SSE gave up after {retry} retries: {error}\n"
|
||||
)
|
||||
elif status == "unsupported":
|
||||
self._sse_label.setText("Not available")
|
||||
self._sse_label.setStyleSheet("font-size: 11px; color: #888;")
|
||||
@@ -2791,11 +2867,6 @@ class SiloAuthDockWidget:
|
||||
"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;",
|
||||
@@ -2815,11 +2886,17 @@ class SiloAuthDockWidget:
|
||||
self._append_activity_event(f"{part_number} updated", part_number)
|
||||
|
||||
def _on_remote_revision(self, part_number, revision):
|
||||
FreeCAD.Console.PrintMessage(f"Silo: New revision {revision} for {part_number}\n")
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Silo: New revision {revision} for {part_number}\n"
|
||||
)
|
||||
mw = FreeCADGui.getMainWindow()
|
||||
if mw is not None:
|
||||
mw.statusBar().showMessage(f"Silo: {part_number} rev {revision} available", 5000)
|
||||
self._append_activity_event(f"{part_number} Rev {revision} created", part_number)
|
||||
mw.statusBar().showMessage(
|
||||
f"Silo: {part_number} rev {revision} available", 5000
|
||||
)
|
||||
self._append_activity_event(
|
||||
f"{part_number} Rev {revision} created", part_number
|
||||
)
|
||||
|
||||
def _append_activity_event(self, text, pn=""):
|
||||
"""Prepend an event to the activity feed and rebuild the display."""
|
||||
@@ -2845,9 +2922,9 @@ class SiloAuthDockWidget:
|
||||
ts = datetime.now()
|
||||
if updated:
|
||||
try:
|
||||
ts = datetime.fromisoformat(updated.replace("Z", "+00:00")).replace(
|
||||
tzinfo=None
|
||||
)
|
||||
ts = datetime.fromisoformat(
|
||||
updated.replace("Z", "+00:00")
|
||||
).replace(tzinfo=None)
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
self._activity_events.insert(0, (ts, text, pn))
|
||||
@@ -2914,7 +2991,9 @@ class SiloAuthDockWidget:
|
||||
self._append_activity_event(f"{status} \u2013 {part_number}", part_number)
|
||||
|
||||
def _on_job_created(self, job_id, definition_name, part_number):
|
||||
FreeCAD.Console.PrintMessage(f"Silo: Job {definition_name} created for {part_number}\n")
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Silo: Job {definition_name} created for {part_number}\n"
|
||||
)
|
||||
self._append_activity_event(
|
||||
f"\u23f3 {part_number} \u2013 {definition_name} queued",
|
||||
part_number,
|
||||
@@ -2929,10 +3008,14 @@ class SiloAuthDockWidget:
|
||||
self._append_activity_event(f"\u2717 Job {job_id[:8]} failed: {error}")
|
||||
|
||||
def _on_job_claimed(self, job_id, runner_id):
|
||||
FreeCAD.Console.PrintMessage(f"Silo: Job {job_id[:8]} claimed by runner {runner_id}\n")
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Silo: Job {job_id[:8]} claimed by runner {runner_id}\n"
|
||||
)
|
||||
|
||||
def _on_job_progress(self, job_id, progress, message):
|
||||
FreeCAD.Console.PrintMessage(f"Silo: Job {job_id[:8]} progress {progress}%: {message}\n")
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Silo: Job {job_id[:8]} progress {progress}%: {message}\n"
|
||||
)
|
||||
|
||||
def _on_job_cancelled(self, job_id):
|
||||
FreeCAD.Console.PrintMessage(f"Silo: Job {job_id[:8]} cancelled\n")
|
||||
@@ -3263,9 +3346,15 @@ class JobMonitorDialog:
|
||||
status = job.get("status", "")
|
||||
icon = _STATUS_ICONS.get(status, "?")
|
||||
self._table.setItem(row, 0, QtGui.QTableWidgetItem(f"{icon} {status}"))
|
||||
self._table.setItem(row, 1, QtGui.QTableWidgetItem(job.get("definition_name", "")))
|
||||
self._table.setItem(row, 2, QtGui.QTableWidgetItem(job.get("part_number", "")))
|
||||
self._table.setItem(row, 3, QtGui.QTableWidgetItem(job.get("runner_name", "")))
|
||||
self._table.setItem(
|
||||
row, 1, QtGui.QTableWidgetItem(job.get("definition_name", ""))
|
||||
)
|
||||
self._table.setItem(
|
||||
row, 2, QtGui.QTableWidgetItem(job.get("part_number", ""))
|
||||
)
|
||||
self._table.setItem(
|
||||
row, 3, QtGui.QTableWidgetItem(job.get("runner_name", ""))
|
||||
)
|
||||
|
||||
progress = job.get("progress", 0)
|
||||
progress_msg = job.get("progress_message", "")
|
||||
@@ -3337,7 +3426,9 @@ class JobMonitorDialog:
|
||||
_client.cancel_job(job_id)
|
||||
FreeCAD.Console.PrintMessage(f"Silo: Job {job_id} cancelled\n")
|
||||
except Exception as e:
|
||||
QtGui.QMessageBox.warning(self.dialog, "Cancel Failed", f"Failed to cancel job:\n{e}")
|
||||
QtGui.QMessageBox.warning(
|
||||
self.dialog, "Cancel Failed", f"Failed to cancel job:\n{e}"
|
||||
)
|
||||
self._refresh()
|
||||
|
||||
def _trigger_job(self):
|
||||
@@ -3346,7 +3437,9 @@ class JobMonitorDialog:
|
||||
try:
|
||||
definitions = _client.list_job_definitions()
|
||||
except Exception as e:
|
||||
QtGui.QMessageBox.warning(self.dialog, "Error", f"Failed to load job definitions:\n{e}")
|
||||
QtGui.QMessageBox.warning(
|
||||
self.dialog, "Error", f"Failed to load job definitions:\n{e}"
|
||||
)
|
||||
return
|
||||
|
||||
if not definitions:
|
||||
@@ -3374,9 +3467,13 @@ class JobMonitorDialog:
|
||||
|
||||
try:
|
||||
result = _client.trigger_job(name, part_number=pn)
|
||||
FreeCAD.Console.PrintMessage(f"Silo: Job triggered: {result.get('id', '')}\n")
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Silo: Job triggered: {result.get('id', '')}\n"
|
||||
)
|
||||
except Exception as e:
|
||||
QtGui.QMessageBox.warning(self.dialog, "Trigger Failed", f"Failed to trigger job:\n{e}")
|
||||
QtGui.QMessageBox.warning(
|
||||
self.dialog, "Trigger Failed", f"Failed to trigger job:\n{e}"
|
||||
)
|
||||
self._refresh()
|
||||
|
||||
def on_job_event(self):
|
||||
@@ -3438,7 +3535,9 @@ class RunnerAdminDialog:
|
||||
# Runner table
|
||||
self._table = QtGui.QTableWidget()
|
||||
self._table.setColumnCount(5)
|
||||
self._table.setHorizontalHeaderLabels(["Name", "Tags", "Status", "Last Heartbeat", "Jobs"])
|
||||
self._table.setHorizontalHeaderLabels(
|
||||
["Name", "Tags", "Status", "Last Heartbeat", "Jobs"]
|
||||
)
|
||||
self._table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
|
||||
self._table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
|
||||
self._table.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
|
||||
@@ -3494,7 +3593,9 @@ class RunnerAdminDialog:
|
||||
def _register_runner(self):
|
||||
from PySide import QtGui
|
||||
|
||||
name, ok = QtGui.QInputDialog.getText(self.dialog, "Register Runner", "Runner name:")
|
||||
name, ok = QtGui.QInputDialog.getText(
|
||||
self.dialog, "Register Runner", "Runner name:"
|
||||
)
|
||||
if not ok or not name:
|
||||
return
|
||||
|
||||
@@ -3820,7 +3921,9 @@ class Silo_StartPanel:
|
||||
dock = QtGui.QDockWidget("Silo", mw)
|
||||
dock.setObjectName("SiloStartPanel")
|
||||
dock.setWidget(content.widget)
|
||||
dock.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea | QtCore.Qt.RightDockWidgetArea)
|
||||
dock.setAllowedAreas(
|
||||
QtCore.Qt.LeftDockWidgetArea | QtCore.Qt.RightDockWidgetArea
|
||||
)
|
||||
mw.addDockWidget(QtCore.Qt.LeftDockWidgetArea, dock)
|
||||
|
||||
def IsActive(self):
|
||||
@@ -3854,7 +3957,9 @@ class _DiagWorker(QtCore.QThread):
|
||||
self.result.emit("DNS", False, "no hostname in URL")
|
||||
return
|
||||
try:
|
||||
addrs = socket.getaddrinfo(hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM)
|
||||
addrs = socket.getaddrinfo(
|
||||
hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM
|
||||
)
|
||||
first_ip = addrs[0][4][0] if addrs else "?"
|
||||
self.result.emit("DNS", True, f"{hostname} -> {first_ip}")
|
||||
except socket.gaierror as e:
|
||||
|
||||
@@ -30,7 +30,7 @@ class SiloOrigin:
|
||||
Key behaviors:
|
||||
- Documents are always stored locally (hybrid local-remote model)
|
||||
- Database tracks metadata, part numbers, and revision history
|
||||
- MinIO stores revision snapshots for sync/backup
|
||||
- Server stores revision files for sync/backup
|
||||
- Identity is tracked by UUID (SiloItemId), displayed as part number
|
||||
"""
|
||||
|
||||
|
||||
Submodule silo-client updated: 5e6f2cb963...f602eee7cc
Reference in New Issue
Block a user