Compare commits
1 Commits
auto/updat
...
feat/dag-a
| Author | SHA1 | Date | |
|---|---|---|---|
| 27ddd6d750 |
@@ -106,9 +106,7 @@ def _register_silo_overlay():
|
||||
return False
|
||||
|
||||
try:
|
||||
from kindred_sdk import register_overlay
|
||||
|
||||
register_overlay(
|
||||
FreeCADGui.registerEditingOverlay(
|
||||
"silo", # overlay id
|
||||
["Silo Origin"], # toolbar names to append
|
||||
_silo_overlay_match, # match function
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -12,17 +12,4 @@
|
||||
<subdirectory>./</subdirectory>
|
||||
</workbench>
|
||||
</content>
|
||||
|
||||
<!-- Kindred Create extensions -->
|
||||
<kindred>
|
||||
<min_create_version>0.1.0</min_create_version>
|
||||
<load_priority>60</load_priority>
|
||||
<pure_python>true</pure_python>
|
||||
<dependencies>
|
||||
<dependency>sdk</dependency>
|
||||
</dependencies>
|
||||
<contexts>
|
||||
<context id="*" action="overlay"/>
|
||||
</contexts>
|
||||
</kindred>
|
||||
</package>
|
||||
|
||||
@@ -10,6 +10,9 @@ backward-compatible :class:`SchemaFormDialog` modal.
|
||||
"""
|
||||
|
||||
import json
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
import FreeCAD
|
||||
from PySide import QtCore, QtGui, QtWidgets
|
||||
@@ -264,8 +267,17 @@ class SchemaFormWidget(QtWidgets.QWidget):
|
||||
|
||||
def _fetch_properties(self, category: str) -> dict:
|
||||
"""Fetch merged property definitions for a category."""
|
||||
from silo_commands import _get_api_url, _get_auth_headers, _get_ssl_context
|
||||
|
||||
api_url = _get_api_url().rstrip("/")
|
||||
url = f"{api_url}/schemas/kindred-rd/properties?category={urllib.parse.quote(category)}"
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
req.add_header("Accept", "application/json")
|
||||
for k, v in _get_auth_headers().items():
|
||||
req.add_header(k, v)
|
||||
try:
|
||||
data = self._client.get_property_schema(category=category)
|
||||
resp = urllib.request.urlopen(req, context=_get_ssl_context(), timeout=5)
|
||||
data = json.loads(resp.read().decode("utf-8"))
|
||||
return data.get("properties", data)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
@@ -275,10 +287,19 @@ class SchemaFormWidget(QtWidgets.QWidget):
|
||||
|
||||
def _generate_pn_preview(self, category: str) -> str:
|
||||
"""Call the server to preview the next part number."""
|
||||
from silo_commands import _get_schema_name
|
||||
from silo_commands import _get_api_url, _get_auth_headers, _get_ssl_context
|
||||
|
||||
api_url = _get_api_url().rstrip("/")
|
||||
url = f"{api_url}/generate-part-number"
|
||||
payload = json.dumps({"schema": "kindred-rd", "category": category}).encode()
|
||||
req = urllib.request.Request(url, data=payload, method="POST")
|
||||
req.add_header("Content-Type", "application/json")
|
||||
req.add_header("Accept", "application/json")
|
||||
for k, v in _get_auth_headers().items():
|
||||
req.add_header(k, v)
|
||||
try:
|
||||
data = self._client.generate_part_number(_get_schema_name(), category)
|
||||
resp = urllib.request.urlopen(req, context=_get_ssl_context(), timeout=5)
|
||||
data = json.loads(resp.read().decode("utf-8"))
|
||||
return data.get("part_number", "")
|
||||
except Exception:
|
||||
return ""
|
||||
@@ -553,10 +574,8 @@ class SchemaFormWidget(QtWidgets.QWidget):
|
||||
return
|
||||
|
||||
try:
|
||||
from silo_commands import _get_schema_name
|
||||
|
||||
result = self._client.create_item(
|
||||
_get_schema_name(),
|
||||
"kindred-rd",
|
||||
data["category"],
|
||||
data["description"],
|
||||
projects=data["projects"],
|
||||
|
||||
@@ -7,14 +7,20 @@ import socket
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
from PySide import QtCore
|
||||
from silo_client import SiloClient, SiloSettings
|
||||
from silo_client import (
|
||||
CATEGORY_NAMES,
|
||||
SiloClient,
|
||||
SiloSettings,
|
||||
get_category_folder_name,
|
||||
parse_part_number,
|
||||
sanitize_filename,
|
||||
)
|
||||
|
||||
# Preference group for Kindred Silo settings
|
||||
_PREF_GROUP = "User parameter:BaseApp/Preferences/Mod/KindredSilo"
|
||||
@@ -25,46 +31,6 @@ SILO_PROJECTS_DIR = os.environ.get(
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Local utility helpers (previously in silo_client, now server-driven)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _parse_part_number(part_number: str) -> Tuple[str, str]:
|
||||
"""Parse part number into ``(category, sequence)``. E.g. ``"F01-0001"`` -> ``("F01", "0001")``."""
|
||||
parts = part_number.split("-")
|
||||
if len(parts) >= 2:
|
||||
return parts[0], parts[1]
|
||||
return part_number, ""
|
||||
|
||||
|
||||
def _sanitize_filename(name: str) -> str:
|
||||
"""Sanitize a string for use in filenames."""
|
||||
sanitized = re.sub(r'[<>:"/\\|?*]', "_", name)
|
||||
sanitized = re.sub(r"[\s_]+", "_", sanitized)
|
||||
sanitized = sanitized.strip("_ ")
|
||||
return sanitized[:50]
|
||||
|
||||
|
||||
def _relative_time(dt):
|
||||
"""Format a datetime as a human-friendly relative string."""
|
||||
now = datetime.now()
|
||||
diff = now - dt
|
||||
seconds = int(diff.total_seconds())
|
||||
if seconds < 60:
|
||||
return "just now"
|
||||
minutes = seconds // 60
|
||||
if minutes < 60:
|
||||
return f"{minutes}m ago"
|
||||
hours = minutes // 60
|
||||
if hours < 24:
|
||||
return f"{hours}h ago"
|
||||
days = hours // 24
|
||||
if days < 30:
|
||||
return f"{days}d ago"
|
||||
return dt.strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# FreeCAD settings adapter
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -110,13 +76,6 @@ class FreeCADSiloSettings(SiloSettings):
|
||||
if token:
|
||||
param.SetString("ApiToken", token)
|
||||
|
||||
def get_schema_name(self) -> str:
|
||||
param = FreeCAD.ParamGet(_PREF_GROUP)
|
||||
name = param.GetString("SchemaName", "")
|
||||
if not name:
|
||||
name = os.environ.get("SILO_SCHEMA", "kindred-rd")
|
||||
return name
|
||||
|
||||
def clear_auth(self):
|
||||
param = FreeCAD.ParamGet(_PREF_GROUP)
|
||||
param.SetString("ApiToken", "")
|
||||
@@ -160,10 +119,6 @@ def _get_api_url() -> str:
|
||||
return _fc_settings.get_api_url()
|
||||
|
||||
|
||||
def _get_schema_name() -> str:
|
||||
return _fc_settings.get_schema_name()
|
||||
|
||||
|
||||
def _get_ssl_verify() -> bool:
|
||||
return _fc_settings.get_ssl_verify()
|
||||
|
||||
@@ -197,13 +152,13 @@ def _clear_auth():
|
||||
# Server mode tracking
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_server_mode = "offline" # "normal" | "read-only" | "offline"
|
||||
_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", "offline".
|
||||
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
|
||||
@@ -218,6 +173,8 @@ 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:
|
||||
@@ -257,22 +214,24 @@ def get_projects_dir() -> Path:
|
||||
def get_cad_file_path(part_number: str, description: str = "") -> Path:
|
||||
"""Generate canonical file path for a CAD file.
|
||||
|
||||
Path format: ~/projects/cad/{category_code}/{part_number}_{description}.kc
|
||||
Path format: ~/projects/cad/{category_code}_{category_name}/{part_number}_{description}.kc
|
||||
"""
|
||||
category, _ = _parse_part_number(part_number)
|
||||
category, _ = parse_part_number(part_number)
|
||||
folder_name = get_category_folder_name(category)
|
||||
|
||||
if description:
|
||||
filename = f"{part_number}_{_sanitize_filename(description)}.kc"
|
||||
filename = f"{part_number}_{sanitize_filename(description)}.kc"
|
||||
else:
|
||||
filename = f"{part_number}.kc"
|
||||
|
||||
return get_projects_dir() / "cad" / category / filename
|
||||
return get_projects_dir() / "cad" / folder_name / filename
|
||||
|
||||
|
||||
def find_file_by_part_number(part_number: str) -> Optional[Path]:
|
||||
"""Find existing CAD file for a part number. Prefers .kc over .FCStd."""
|
||||
category, _ = _parse_part_number(part_number)
|
||||
cad_dir = get_projects_dir() / "cad" / category
|
||||
category, _ = parse_part_number(part_number)
|
||||
folder_name = get_category_folder_name(category)
|
||||
cad_dir = get_projects_dir() / "cad" / folder_name
|
||||
|
||||
for search_dir in _search_dirs(cad_dir):
|
||||
for ext in ("*.kc", "*.FCStd"):
|
||||
@@ -537,7 +496,7 @@ class SiloSync:
|
||||
)
|
||||
|
||||
# Add a Body for parts (not assemblies)
|
||||
body_label = _sanitize_filename(description) if description else "Body"
|
||||
body_label = sanitize_filename(description) if description else "Body"
|
||||
body = doc.addObject("PartDesign::Body", "_" + body_label)
|
||||
body.Label = body_label
|
||||
part_obj.addObject(body)
|
||||
@@ -568,7 +527,7 @@ class SiloSync:
|
||||
def upload_file(
|
||||
self, part_number: str, file_path: str, comment: str = "Auto-save"
|
||||
) -> Optional[Dict]:
|
||||
"""Upload file to the server."""
|
||||
"""Upload file to MinIO."""
|
||||
try:
|
||||
doc = FreeCAD.openDocument(file_path)
|
||||
if not doc:
|
||||
@@ -582,7 +541,7 @@ class SiloSync:
|
||||
return None
|
||||
|
||||
def download_file(self, part_number: str) -> Optional[Path]:
|
||||
"""Download the latest revision file from the server."""
|
||||
"""Download latest file from MinIO."""
|
||||
try:
|
||||
item = self.client.get_item(part_number)
|
||||
file_path = get_cad_file_path(part_number, item.get("description", ""))
|
||||
@@ -781,12 +740,12 @@ def _push_dag_after_upload(doc, part_number, revision_number):
|
||||
|
||||
|
||||
class Silo_Save:
|
||||
"""Save locally and upload to the server."""
|
||||
"""Save locally and upload to MinIO."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"MenuText": "Save",
|
||||
"ToolTip": "Save locally and upload to server (Ctrl+S)",
|
||||
"ToolTip": "Save locally and upload to MinIO (Ctrl+S)",
|
||||
"Pixmap": _icon("save"),
|
||||
}
|
||||
|
||||
@@ -841,7 +800,7 @@ class Silo_Save:
|
||||
|
||||
FreeCAD.Console.PrintMessage(f"Saved: {file_path}\n")
|
||||
|
||||
# Try to upload to server
|
||||
# Try to upload to MinIO
|
||||
try:
|
||||
result = _client._upload_file(
|
||||
part_number, str(file_path), properties, "Auto-save"
|
||||
@@ -1120,12 +1079,12 @@ def _pull_dependencies(part_number, progress_callback=None):
|
||||
|
||||
|
||||
class Silo_Pull:
|
||||
"""Download revision file from the server."""
|
||||
"""Download from MinIO / sync from database."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"MenuText": "Pull",
|
||||
"ToolTip": "Download file with revision selection",
|
||||
"ToolTip": "Download from MinIO with revision selection",
|
||||
"Pixmap": _icon("pull"),
|
||||
}
|
||||
|
||||
@@ -1281,12 +1240,12 @@ class Silo_Pull:
|
||||
|
||||
|
||||
class Silo_Push:
|
||||
"""Upload local files to the server."""
|
||||
"""Upload local files to MinIO."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"MenuText": "Push",
|
||||
"ToolTip": "Upload local files that aren't on the server",
|
||||
"ToolTip": "Upload local files that aren't in MinIO",
|
||||
"Pixmap": _icon("push"),
|
||||
}
|
||||
|
||||
@@ -1407,7 +1366,7 @@ class Silo_Info:
|
||||
msg += f"<p><b>Local Revision:</b> {getattr(obj, 'SiloRevision', '-')}</p>"
|
||||
|
||||
has_file, _ = _client.has_file(part_number)
|
||||
msg += f"<p><b>File on Server:</b> {'Yes' if has_file else 'No'}</p>"
|
||||
msg += f"<p><b>File in MinIO:</b> {'Yes' if has_file else 'No'}</p>"
|
||||
|
||||
# Show current revision status
|
||||
if revisions:
|
||||
@@ -1790,31 +1749,6 @@ class Silo_Settings:
|
||||
|
||||
layout.addSpacing(10)
|
||||
|
||||
# Schema name
|
||||
schema_label = QtGui.QLabel("Schema Name:")
|
||||
layout.addWidget(schema_label)
|
||||
|
||||
schema_input = QtGui.QLineEdit()
|
||||
schema_input.setPlaceholderText("kindred-rd")
|
||||
current_schema = param.GetString("SchemaName", "")
|
||||
if current_schema:
|
||||
schema_input.setText(current_schema)
|
||||
else:
|
||||
env_schema = os.environ.get("SILO_SCHEMA", "")
|
||||
if env_schema:
|
||||
schema_input.setText(env_schema)
|
||||
layout.addWidget(schema_input)
|
||||
|
||||
schema_hint = QtGui.QLabel(
|
||||
"The part-numbering schema to use. Leave empty for "
|
||||
"SILO_SCHEMA env var or default (kindred-rd)."
|
||||
)
|
||||
schema_hint.setWordWrap(True)
|
||||
schema_hint.setStyleSheet("color: #888; font-size: 11px;")
|
||||
layout.addWidget(schema_hint)
|
||||
|
||||
layout.addSpacing(10)
|
||||
|
||||
# SSL
|
||||
ssl_checkbox = QtGui.QCheckBox("Verify SSL certificates")
|
||||
ssl_checkbox.setChecked(param.GetBool("SslVerify", True))
|
||||
@@ -1957,7 +1891,6 @@ class Silo_Settings:
|
||||
auth_display = "not configured"
|
||||
status_label = QtGui.QLabel(
|
||||
f"<b>Active URL:</b> {_get_api_url()}<br>"
|
||||
f"<b>Schema:</b> {_get_schema_name()}<br>"
|
||||
f"<b>SSL verification:</b> {'enabled' if _get_ssl_verify() else 'disabled'}<br>"
|
||||
f"<b>CA certificate:</b> {cert_display}<br>"
|
||||
f"<b>Authentication:</b> {auth_display}"
|
||||
@@ -1979,7 +1912,6 @@ class Silo_Settings:
|
||||
def on_save():
|
||||
url = url_input.text().strip()
|
||||
param.SetString("ApiUrl", url)
|
||||
param.SetString("SchemaName", schema_input.text().strip())
|
||||
param.SetBool("SslVerify", ssl_checkbox.isChecked())
|
||||
cert_path = cert_input.text().strip()
|
||||
param.SetString("SslCertPath", cert_path)
|
||||
@@ -2404,7 +2336,7 @@ class SiloEventListener(QtCore.QThread):
|
||||
connection_status = QtCore.Signal(
|
||||
str, int, str
|
||||
) # (status, retry_count, error_message)
|
||||
server_mode_changed = QtCore.Signal(str) # "normal" / "read-only" / "offline"
|
||||
server_mode_changed = QtCore.Signal(str) # "normal" / "read-only" / "degraded"
|
||||
|
||||
# DAG events
|
||||
dag_updated = QtCore.Signal(str, int, int) # part_number, node_count, edge_count
|
||||
@@ -2603,8 +2535,6 @@ class SiloAuthDockWidget:
|
||||
|
||||
self.widget = QtGui.QWidget()
|
||||
self._event_listener = None
|
||||
self._activity_events = [] # list of (datetime, text, part_number)
|
||||
self._activity_seeded = False
|
||||
self._build_ui()
|
||||
self._refresh_status()
|
||||
|
||||
@@ -2612,11 +2542,6 @@ class SiloAuthDockWidget:
|
||||
self._timer.timeout.connect(self._refresh_status)
|
||||
self._timer.start(30000)
|
||||
|
||||
# Refresh relative timestamps every 60s
|
||||
self._ts_timer = QtCore.QTimer(self.widget)
|
||||
self._ts_timer.timeout.connect(self._rebuild_activity_feed)
|
||||
self._ts_timer.start(60000)
|
||||
|
||||
# -- UI construction ----------------------------------------------------
|
||||
|
||||
def _build_ui(self):
|
||||
@@ -2715,11 +2640,7 @@ class SiloAuthDockWidget:
|
||||
btn_row.addWidget(settings_btn)
|
||||
|
||||
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
|
||||
)
|
||||
layout.addStretch()
|
||||
|
||||
# -- Status refresh -----------------------------------------------------
|
||||
|
||||
@@ -2831,7 +2752,6 @@ class SiloAuthDockWidget:
|
||||
self._sse_label.setStyleSheet("font-size: 11px; color: #4CAF50;")
|
||||
self._sse_label.setToolTip("")
|
||||
FreeCAD.Console.PrintMessage("Silo: SSE connected\n")
|
||||
self._seed_activity_feed()
|
||||
elif status == "disconnected":
|
||||
self._sse_label.setText(
|
||||
f"Reconnecting ({retry}/{SiloEventListener._MAX_RETRIES})..."
|
||||
@@ -2856,8 +2776,6 @@ class SiloAuthDockWidget:
|
||||
global _server_mode
|
||||
_server_mode = mode
|
||||
self._update_mode_banner()
|
||||
if mode != "normal":
|
||||
self._append_activity_event(f"Server mode: {mode}")
|
||||
|
||||
def _update_mode_banner(self):
|
||||
_MODE_BANNERS = {
|
||||
@@ -2867,6 +2785,11 @@ 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;",
|
||||
@@ -2883,7 +2806,7 @@ class SiloAuthDockWidget:
|
||||
mw = FreeCADGui.getMainWindow()
|
||||
if mw is not None:
|
||||
mw.statusBar().showMessage(f"Silo: {part_number} updated on server", 5000)
|
||||
self._append_activity_event(f"{part_number} updated", part_number)
|
||||
self._refresh_activity_panel()
|
||||
|
||||
def _on_remote_revision(self, part_number, revision):
|
||||
FreeCAD.Console.PrintMessage(
|
||||
@@ -2894,48 +2817,89 @@ class SiloAuthDockWidget:
|
||||
mw.statusBar().showMessage(
|
||||
f"Silo: {part_number} rev {revision} available", 5000
|
||||
)
|
||||
self._append_activity_event(
|
||||
f"{part_number} Rev {revision} created", part_number
|
||||
self._refresh_activity_panel()
|
||||
|
||||
def _on_dag_updated(self, part_number, node_count, edge_count):
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Silo: DAG updated for {part_number}"
|
||||
f" ({node_count} nodes, {edge_count} edges)\n"
|
||||
)
|
||||
self._add_activity_entry(
|
||||
f"\u25b6 {part_number} \u2013 DAG synced"
|
||||
f" ({node_count} nodes, {edge_count} edges)",
|
||||
part_number,
|
||||
)
|
||||
|
||||
def _append_activity_event(self, text, pn=""):
|
||||
"""Prepend an event to the activity feed and rebuild the display."""
|
||||
self._activity_events.insert(0, (datetime.now(), text, pn))
|
||||
self._activity_events = self._activity_events[:50]
|
||||
self._rebuild_activity_feed()
|
||||
def _on_dag_validated(self, part_number, valid, failed_count):
|
||||
if valid:
|
||||
status = "\u2713 PASS"
|
||||
FreeCAD.Console.PrintMessage(f"Silo: Validation passed for {part_number}\n")
|
||||
else:
|
||||
status = f"\u2717 FAIL ({failed_count} failed)"
|
||||
FreeCAD.Console.PrintWarning(
|
||||
f"Silo: Validation failed for {part_number}"
|
||||
f" ({failed_count} features failed)\n"
|
||||
)
|
||||
self._add_activity_entry(f"{status} \u2013 {part_number}", part_number)
|
||||
|
||||
def _seed_activity_feed(self):
|
||||
"""One-time: populate the feed with recent items from the database."""
|
||||
if self._activity_seeded:
|
||||
def _on_job_created(self, job_id, definition_name, part_number):
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Silo: Job {definition_name} created for {part_number}\n"
|
||||
)
|
||||
self._add_activity_entry(
|
||||
f"\u23f3 {part_number} \u2013 {definition_name} queued",
|
||||
part_number,
|
||||
)
|
||||
|
||||
def _on_job_completed(self, job_id):
|
||||
FreeCAD.Console.PrintMessage(f"Silo: Job {job_id} completed\n")
|
||||
self._refresh_activity_panel()
|
||||
|
||||
def _on_job_failed(self, job_id, error):
|
||||
FreeCAD.Console.PrintError(f"Silo: Job {job_id} failed: {error}\n")
|
||||
self._add_activity_entry(f"\u2717 Job {job_id[:8]} failed: {error}", None)
|
||||
|
||||
def _on_job_claimed(self, job_id, runner_id):
|
||||
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"
|
||||
)
|
||||
|
||||
def _on_job_cancelled(self, job_id):
|
||||
FreeCAD.Console.PrintMessage(f"Silo: Job {job_id[:8]} cancelled\n")
|
||||
self._add_activity_entry(f"\u2718 Job {job_id[:8]} cancelled", None)
|
||||
|
||||
def _add_activity_entry(self, text, part_number):
|
||||
"""Insert a live event entry at the top of the Activity panel."""
|
||||
from PySide import QtCore, QtGui, QtWidgets
|
||||
|
||||
mw = FreeCADGui.getMainWindow()
|
||||
if mw is None:
|
||||
return
|
||||
panel = mw.findChild(QtWidgets.QDockWidget, "SiloDatabaseActivity")
|
||||
if panel is None:
|
||||
return
|
||||
activity_list = panel.findChild(QtWidgets.QListWidget)
|
||||
if activity_list is None:
|
||||
return
|
||||
self._activity_seeded = True
|
||||
try:
|
||||
items = _client.list_items()
|
||||
if isinstance(items, list):
|
||||
for item in reversed(items[:10]):
|
||||
pn = item.get("part_number", "")
|
||||
desc = item.get("description", "")
|
||||
if desc and len(desc) > 40:
|
||||
desc = desc[:37] + "..."
|
||||
text = f"{pn} \u2013 {desc}" if desc else pn
|
||||
updated = item.get("updated_at", "")
|
||||
ts = datetime.now()
|
||||
if updated:
|
||||
try:
|
||||
ts = datetime.fromisoformat(
|
||||
updated.replace("Z", "+00:00")
|
||||
).replace(tzinfo=None)
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
self._activity_events.insert(0, (ts, text, pn))
|
||||
self._activity_events = self._activity_events[:50]
|
||||
except Exception:
|
||||
pass
|
||||
self._rebuild_activity_feed()
|
||||
|
||||
def _rebuild_activity_feed(self):
|
||||
"""Render _activity_events into the Database Activity QListWidget."""
|
||||
from PySide import QtCore, QtWidgets
|
||||
item = QtWidgets.QListWidgetItem(text)
|
||||
if part_number:
|
||||
item.setData(QtCore.Qt.UserRole, part_number)
|
||||
item.setForeground(QtGui.QColor("#89b4fa"))
|
||||
activity_list.insertItem(0, item)
|
||||
|
||||
# Cap the list at 50 entries
|
||||
while activity_list.count() > 50:
|
||||
activity_list.takeItem(activity_list.count() - 1)
|
||||
|
||||
def _refresh_activity_panel(self):
|
||||
"""Refresh the Database Activity panel if it exists."""
|
||||
from PySide import QtCore, QtGui, QtWidgets
|
||||
|
||||
mw = FreeCADGui.getMainWindow()
|
||||
if mw is None:
|
||||
@@ -2957,69 +2921,66 @@ class SiloAuthDockWidget:
|
||||
)
|
||||
activity_list._silo_connected = True
|
||||
|
||||
if not self._activity_events:
|
||||
item = QtWidgets.QListWidgetItem("(No activity yet)")
|
||||
item.setFlags(QtCore.Qt.NoItemFlags)
|
||||
activity_list.addItem(item)
|
||||
return
|
||||
# Collect local part numbers for badge
|
||||
local_pns = set()
|
||||
try:
|
||||
for lf in search_local_files():
|
||||
local_pns.add(lf.get("part_number", ""))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for ts, text, pn in self._activity_events:
|
||||
label = f"{text} \u00b7 {_relative_time(ts)}"
|
||||
list_item = QtWidgets.QListWidgetItem(label)
|
||||
if pn:
|
||||
list_item.setData(QtCore.Qt.UserRole, pn)
|
||||
activity_list.addItem(list_item)
|
||||
try:
|
||||
items = _client.list_items()
|
||||
if isinstance(items, list):
|
||||
for item in items[:20]:
|
||||
pn = item.get("part_number", "")
|
||||
desc = item.get("description", "")
|
||||
updated = item.get("updated_at", "")
|
||||
if updated:
|
||||
updated = updated[:10]
|
||||
|
||||
def _on_dag_updated(self, part_number, node_count, edge_count):
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Silo: DAG updated for {part_number} ({node_count} nodes, {edge_count} edges)\n"
|
||||
)
|
||||
self._append_activity_event(
|
||||
f"\u25b6 {part_number} \u2013 DAG synced ({node_count} nodes, {edge_count} edges)",
|
||||
part_number,
|
||||
)
|
||||
# Fetch latest revision info
|
||||
rev_num = ""
|
||||
comment = ""
|
||||
try:
|
||||
revs = _client.get_revisions(pn)
|
||||
if revs:
|
||||
latest = revs[0] if isinstance(revs, list) else revs
|
||||
rev_num = str(latest.get("revision_number", ""))
|
||||
comment = latest.get("comment", "") or ""
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_dag_validated(self, part_number, valid, failed_count):
|
||||
if valid:
|
||||
status = "\u2713 PASS"
|
||||
FreeCAD.Console.PrintMessage(f"Silo: Validation passed for {part_number}\n")
|
||||
else:
|
||||
status = f"\u2717 FAIL ({failed_count} failed)"
|
||||
FreeCAD.Console.PrintWarning(
|
||||
f"Silo: Validation failed for {part_number} ({failed_count} features failed)\n"
|
||||
)
|
||||
self._append_activity_event(f"{status} \u2013 {part_number}", part_number)
|
||||
# Truncate long descriptions
|
||||
desc_display = desc
|
||||
if len(desc_display) > 40:
|
||||
desc_display = desc_display[:37] + "..."
|
||||
|
||||
def _on_job_created(self, job_id, definition_name, part_number):
|
||||
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,
|
||||
)
|
||||
# Build display text
|
||||
rev_part = f" \u2013 Rev {rev_num}" if rev_num else ""
|
||||
date_part = f" \u2013 {updated}" if updated else ""
|
||||
local_badge = " \u25cf local" if pn in local_pns else ""
|
||||
line1 = (
|
||||
f"{pn} \u2013 {desc_display}{rev_part}{date_part}{local_badge}"
|
||||
)
|
||||
|
||||
def _on_job_completed(self, job_id):
|
||||
FreeCAD.Console.PrintMessage(f"Silo: Job {job_id} completed\n")
|
||||
self._rebuild_activity_feed()
|
||||
if comment:
|
||||
line1 += f'\n "{comment}"'
|
||||
else:
|
||||
line1 += "\n (no comment)"
|
||||
|
||||
def _on_job_failed(self, job_id, error):
|
||||
FreeCAD.Console.PrintError(f"Silo: Job {job_id} failed: {error}\n")
|
||||
self._append_activity_event(f"\u2717 Job {job_id[:8]} failed: {error}")
|
||||
list_item = QtWidgets.QListWidgetItem(line1)
|
||||
list_item.setData(QtCore.Qt.UserRole, pn)
|
||||
if desc and len(desc) > 40:
|
||||
list_item.setToolTip(desc)
|
||||
if pn in local_pns:
|
||||
list_item.setForeground(QtGui.QColor("#4CAF50"))
|
||||
activity_list.addItem(list_item)
|
||||
|
||||
def _on_job_claimed(self, job_id, runner_id):
|
||||
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"
|
||||
)
|
||||
|
||||
def _on_job_cancelled(self, job_id):
|
||||
FreeCAD.Console.PrintMessage(f"Silo: Job {job_id[:8]} cancelled\n")
|
||||
self._append_activity_event(f"\u2718 Job {job_id[:8]} cancelled")
|
||||
if activity_list.count() == 0:
|
||||
activity_list.addItem("(No items in database)")
|
||||
except Exception:
|
||||
activity_list.addItem("(Unable to refresh activity)")
|
||||
|
||||
def _on_activity_double_click(self, item):
|
||||
"""Open/checkout item from activity pane."""
|
||||
@@ -3417,7 +3378,8 @@ class JobMonitorDialog:
|
||||
reply = QtGui.QMessageBox.question(
|
||||
self.dialog,
|
||||
"Cancel Job",
|
||||
f"Cancel job {job.get('definition_name', '')} for {job.get('part_number', '')}?",
|
||||
f"Cancel job {job.get('definition_name', '')} for "
|
||||
f"{job.get('part_number', '')}?",
|
||||
QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
|
||||
)
|
||||
if reply != QtGui.QMessageBox.Yes:
|
||||
|
||||
@@ -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
|
||||
- Server stores revision files for sync/backup
|
||||
- MinIO stores revision snapshots for sync/backup
|
||||
- Identity is tracked by UUID (SiloItemId), displayed as part number
|
||||
"""
|
||||
|
||||
@@ -388,7 +388,9 @@ class SiloOrigin:
|
||||
|
||||
# Upload to Silo
|
||||
properties = collect_document_properties(doc)
|
||||
_client._upload_file(obj.SiloPartNumber, str(file_path), properties, comment="")
|
||||
_client._upload_file(
|
||||
obj.SiloPartNumber, str(file_path), properties, comment=""
|
||||
)
|
||||
|
||||
# Clear modified flag (Modified is on Gui.Document, not App.Document)
|
||||
gui_doc = FreeCADGui.getDocument(doc.Name)
|
||||
@@ -565,9 +567,12 @@ def register_silo_origin():
|
||||
This should be called during workbench initialization to make
|
||||
Silo available as a file origin.
|
||||
"""
|
||||
from kindred_sdk import register_origin
|
||||
|
||||
register_origin(get_silo_origin())
|
||||
origin = get_silo_origin()
|
||||
try:
|
||||
FreeCADGui.addOrigin(origin)
|
||||
FreeCAD.Console.PrintLog("Registered Silo origin\n")
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintWarning(f"Could not register Silo origin: {e}\n")
|
||||
|
||||
|
||||
def unregister_silo_origin():
|
||||
@@ -577,7 +582,9 @@ def unregister_silo_origin():
|
||||
"""
|
||||
global _silo_origin
|
||||
if _silo_origin:
|
||||
from kindred_sdk import unregister_origin
|
||||
|
||||
unregister_origin(_silo_origin)
|
||||
try:
|
||||
FreeCADGui.removeOrigin(_silo_origin)
|
||||
FreeCAD.Console.PrintLog("Unregistered Silo origin\n")
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintWarning(f"Could not unregister Silo origin: {e}\n")
|
||||
_silo_origin = None
|
||||
|
||||
@@ -19,10 +19,23 @@ from PySide import QtCore, QtGui, QtWidgets
|
||||
# ---------------------------------------------------------------------------
|
||||
# Catppuccin Mocha palette
|
||||
# ---------------------------------------------------------------------------
|
||||
# Catppuccin Mocha palette — sourced from kindred-addon-sdk
|
||||
from kindred_sdk.theme import get_theme_tokens
|
||||
|
||||
_MOCHA = get_theme_tokens()
|
||||
_MOCHA = {
|
||||
"base": "#1e1e2e",
|
||||
"mantle": "#181825",
|
||||
"crust": "#11111b",
|
||||
"surface0": "#313244",
|
||||
"surface1": "#45475a",
|
||||
"surface2": "#585b70",
|
||||
"text": "#cdd6f4",
|
||||
"subtext0": "#a6adc8",
|
||||
"subtext1": "#bac2de",
|
||||
"blue": "#89b4fa",
|
||||
"green": "#a6e3a1",
|
||||
"red": "#f38ba8",
|
||||
"peach": "#fab387",
|
||||
"lavender": "#b4befe",
|
||||
"overlay0": "#6c7086",
|
||||
}
|
||||
|
||||
_PREF_GROUP = "User parameter:BaseApp/Preferences/Mod/KindredSilo"
|
||||
|
||||
|
||||
Submodule silo-client updated: 285bd1fa11...9b71cf0375
Reference in New Issue
Block a user