Compare commits

..

1 Commits

10 changed files with 231 additions and 245 deletions

View File

@@ -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

View File

@@ -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>

View File

@@ -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"],

View File

@@ -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:

View File

@@ -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

View File

@@ -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"