From 1ff51aef2e239e63914ba3a3aa4deaadbbc6a6e0 Mon Sep 17 00:00:00 2001 From: X9X0 Date: Mon, 15 Dec 2025 11:41:15 -0500 Subject: [PATCH 1/6] fix: Add USB diagnostics utility to troubleshoot unreadable serial numbers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Issue #166 where BK 9205B connection fails due to unreadable USB serial numbers after extended server uptime. Changes: - Created comprehensive USB diagnostics utility (server/utils/usb_diagnostics.py) - Added /equipment/diagnostics/usb API endpoint for device analysis - Integrated diagnostics button into client Diagnostics panel - Added automatic diagnostic logging to BK power supply connection errors - Provides detailed root cause analysis and troubleshooting recommendations The diagnostics tool helps identify why USB serial numbers become unreadable (showing as ???) and provides actionable steps to resolve the issue, such as unplugging/replugging the device or restarting the server. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- client/api/client.py | 26 ++++++ client/ui/diagnostics_panel.py | 138 ++++++++++++++++++++++++++++ server/api/equipment.py | 20 ++++ server/equipment/bk_power_supply.py | 22 +++++ server/utils/usb_diagnostics.py | 105 +++++++++++++++++++++ 5 files changed, 311 insertions(+) create mode 100644 server/utils/usb_diagnostics.py diff --git a/client/api/client.py b/client/api/client.py index e0b1051..df67b1e 100644 --- a/client/api/client.py +++ b/client/api/client.py @@ -1401,6 +1401,32 @@ def run_pi_diagnostics(self) -> Dict[str, Any]: response.raise_for_status() return response.json() + def run_usb_diagnostics(self, resource_string: str) -> Dict[str, Any]: + """Run USB device diagnostics to troubleshoot connection issues. + + Analyzes why a USB device's serial number may not be readable and + provides recommendations for resolving the issue. + + Args: + resource_string: VISA resource string of the device to diagnose + + Returns: + Dictionary containing: + - resource_string: The analyzed resource string + - has_serial: Whether a serial number is present + - serial_readable: Whether the serial number can be read + - usb_info: USB vendor/product/serial information + - issues: List of detected issues + - recommendations: List of recommended fixes + """ + response = self._session.post( + f"{self.base_url}/equipment/diagnostics/usb", + params={"resource_string": resource_string}, + timeout=10 + ) + response.raise_for_status() + return response.json() + # ==================== State Management API ==================== def capture_state( diff --git a/client/ui/diagnostics_panel.py b/client/ui/diagnostics_panel.py index 2b87970..750772f 100644 --- a/client/ui/diagnostics_panel.py +++ b/client/ui/diagnostics_panel.py @@ -71,6 +71,11 @@ def _setup_ui(self): pi_diagnostics_btn.setToolTip("Run comprehensive diagnostic script on the Raspberry Pi server") button_layout.addWidget(pi_diagnostics_btn) + usb_diagnostics_btn = QPushButton("USB Device Diagnostics") + usb_diagnostics_btn.clicked.connect(self.run_usb_diagnostics) + usb_diagnostics_btn.setToolTip("Diagnose USB connection issues and unreadable serial numbers") + button_layout.addWidget(usb_diagnostics_btn) + layout.addLayout(button_layout) def set_client(self, client: LabLinkClient): @@ -339,3 +344,136 @@ def save_output(): f"Failed to run Pi diagnostics:\n\n{str(e)}\n\n" "Make sure the diagnose-pi.sh script is installed on the server." ) + + def run_usb_diagnostics(self): + """Run USB device diagnostics to troubleshoot connection issues.""" + if not self.client: + QMessageBox.warning( + self, "Not Connected", "Please connect to a server first" + ) + return + + # Ask user for resource string + from PyQt6.QtWidgets import QInputDialog, QComboBox, QDialog, QVBoxLayout, QLabel + + # Get list of discovered devices to help user choose + try: + devices_response = self.client.discover_devices() + devices = devices_response.get("devices", []) + + # Create dialog with device selection + dialog = QDialog(self) + dialog.setWindowTitle("Select Device for USB Diagnostics") + dialog.resize(600, 200) + + layout = QVBoxLayout(dialog) + + layout.addWidget(QLabel("Select a device to diagnose:")) + + device_combo = QComboBox() + for device in devices: + resource_name = device.get("resource_name", "") + if resource_name.startswith("USB"): + # Show resource name and any available device info + label = resource_name + if device.get("manufacturer"): + label += f" ({device['manufacturer']})" + device_combo.addItem(label, resource_name) + + if device_combo.count() == 0: + device_combo.addItem("No USB devices found", "") + + layout.addWidget(device_combo) + + # Add manual entry option + layout.addWidget(QLabel("\nOr enter a resource string manually:")) + from PyQt6.QtWidgets import QLineEdit + manual_input = QLineEdit() + manual_input.setPlaceholderText("e.g., USB0::11975::37376::800886011797210043::0::INSTR") + layout.addWidget(manual_input) + + # Buttons + from PyQt6.QtWidgets import QDialogButtonBox + button_box = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) + button_box.accepted.connect(dialog.accept) + button_box.rejected.connect(dialog.reject) + layout.addWidget(button_box) + + if dialog.exec() == QDialog.DialogCode.Accepted: + # Use manual input if provided, otherwise use selected device + resource_string = manual_input.text().strip() + if not resource_string: + resource_string = device_combo.currentData() + + if not resource_string: + QMessageBox.warning(self, "No Device", "Please select or enter a device") + return + + # Run diagnostics + try: + diagnostics = self.client.run_usb_diagnostics(resource_string) + + # Format and display results + message = f"USB Device Diagnostics\n" + message += f"{'=' * 50}\n\n" + message += f"Resource String: {diagnostics.get('resource_string', 'N/A')}\n\n" + + usb_info = diagnostics.get('usb_info') + if usb_info: + message += f"USB Information:\n" + message += f" Vendor ID: {usb_info.get('vendor_id', 'N/A')}\n" + message += f" Product ID: {usb_info.get('product_id', 'N/A')}\n" + message += f" Serial Number: {usb_info.get('serial_number', 'N/A')}\n\n" + + message += f"Serial Readable: {'Yes' if diagnostics.get('serial_readable') else 'No'}\n\n" + + issues = diagnostics.get('issues', []) + if issues: + message += f"Issues Detected:\n" + for issue in issues: + message += f" • {issue}\n" + message += "\n" + + recommendations = diagnostics.get('recommendations', []) + if recommendations: + message += f"Recommendations:\n" + for rec in recommendations: + message += f" • {rec}\n" + + # Show results in a scrollable dialog + from PyQt6.QtWidgets import QTextEdit + result_dialog = QDialog(self) + result_dialog.setWindowTitle("USB Diagnostics Results") + result_dialog.resize(700, 500) + + result_layout = QVBoxLayout(result_dialog) + + text_edit = QTextEdit() + text_edit.setReadOnly(True) + text_edit.setPlainText(message) + text_edit.setFontFamily("Monospace") + result_layout.addWidget(text_edit) + + close_btn = QPushButton("Close") + close_btn.clicked.connect(result_dialog.accept) + result_layout.addWidget(close_btn) + + result_dialog.exec() + + except Exception as e: + logger.error(f"Error running USB diagnostics: {e}") + QMessageBox.critical( + self, + "Error", + f"Failed to run USB diagnostics:\n\n{str(e)}" + ) + + except Exception as e: + logger.error(f"Error preparing USB diagnostics: {e}") + QMessageBox.critical( + self, + "Error", + f"Failed to prepare USB diagnostics:\n\n{str(e)}" + ) diff --git a/server/api/equipment.py b/server/api/equipment.py index fbcc235..473bb8c 100644 --- a/server/api/equipment.py +++ b/server/api/equipment.py @@ -74,6 +74,26 @@ async def discover_devices(): raise HTTPException(status_code=500, detail=str(e)) +@router.post("/diagnostics/usb") +async def run_usb_diagnostics(resource_string: str): + """ + Run USB diagnostics on a device to troubleshoot connection issues. + + Helps identify why USB serial numbers may be unreadable and provides + recommendations for resolving the issue. + """ + try: + from server.utils.usb_diagnostics import diagnose_usb_device + + diagnostics = diagnose_usb_device(resource_string) + logger.info(f"USB diagnostics run for {resource_string}") + + return diagnostics + except Exception as e: + logger.error(f"Error running USB diagnostics: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @router.post("/connect", response_model=dict) async def connect_device(request: ConnectDeviceRequest): """Connect to a device.""" diff --git a/server/equipment/bk_power_supply.py b/server/equipment/bk_power_supply.py index fa55dd1..2005c2b 100644 --- a/server/equipment/bk_power_supply.py +++ b/server/equipment/bk_power_supply.py @@ -506,6 +506,28 @@ async def connect(self): self.cached_info = await self.get_info() except Exception as e: + error_msg = str(e).lower() + + # If device not found, provide diagnostic information + if "not found" in error_msg or "no device" in error_msg: + from server.utils.usb_diagnostics import log_usb_diagnostics + logger.error(f"Device not found at {self.resource_string}") + log_usb_diagnostics(self.resource_string) + logger.error( + "\n" + "=" * 70 + "\n" + "USB DEVICE CONNECTION FAILED\n" + "=" * 70 + "\n" + "If the serial number is unreadable (shows as ???), this indicates\n" + "a USB communication issue after long server uptime.\n\n" + "COMMON FIXES:\n" + " 1. Unplug and replug the USB device\n" + " 2. Restart the LabLink server\n" + " 3. Check USB cable and connection quality\n" + " 4. Update device firmware if available\n" + " 5. Run 'USB Device Diagnostics' from the client's Diagnostic tab\n" + "=" * 70 + ) + logger.error(f"Failed to connect to {self.resource_string}: {e}") self.connected = False raise diff --git a/server/utils/usb_diagnostics.py b/server/utils/usb_diagnostics.py new file mode 100644 index 0000000..1b9f4fb --- /dev/null +++ b/server/utils/usb_diagnostics.py @@ -0,0 +1,105 @@ +"""USB Device Diagnostics for troubleshooting serial number reading issues.""" + +import logging +from typing import Dict, List, Optional + +logger = logging.getLogger(__name__) + + +def diagnose_usb_device(resource_string: str) -> Dict[str, any]: + """ + Diagnose USB device and provide detailed information about why + serial number might not be readable. + + Args: + resource_string: VISA resource string of the device + + Returns: + Dictionary with diagnostic information + """ + diagnostics = { + "resource_string": resource_string, + "has_serial": False, + "serial_readable": False, + "usb_info": None, + "issues": [], + "recommendations": [] + } + + # Check if this is a USB device + if not resource_string.startswith("USB"): + diagnostics["issues"].append("Not a USB device") + return diagnostics + + # Parse USB resource string + # Format: USB[board]::vendor::product::serial::interface::INSTR + parts = resource_string.split("::") + if len(parts) < 5: + diagnostics["issues"].append(f"Invalid USB resource string format: {resource_string}") + return diagnostics + + vendor_id = parts[1] if len(parts) > 1 else None + product_id = parts[2] if len(parts) > 2 else None + serial = parts[3] if len(parts) > 3 else None + + diagnostics["usb_info"] = { + "vendor_id": vendor_id, + "product_id": product_id, + "serial_number": serial + } + + # Check if serial number is present and readable + if serial and serial != "???": + diagnostics["has_serial"] = True + diagnostics["serial_readable"] = True + elif serial == "???": + diagnostics["has_serial"] = False + diagnostics["serial_readable"] = False + diagnostics["issues"].append("USB serial number descriptor cannot be read") + + # Add detailed root cause analysis + diagnostics["issues"].extend([ + "Possible causes:", + "1. USB communication issue - device may need to be unplugged/replugged", + "2. Long server uptime - USB subsystem may be in stale state", + "3. Device firmware issue - serial number descriptor may be corrupt", + "4. PyUSB/libusb backend issue - driver may not support reading this descriptor", + "5. USB permissions - process may not have permission to read device descriptors" + ]) + + # Add recommendations + diagnostics["recommendations"].extend([ + "Try unplugging and replugging the USB device", + "Restart the LabLink server to refresh USB subsystem", + "Check USB cable and connection quality", + "Update device firmware if available", + "On Linux: check udev rules and user permissions for USB devices", + "On Windows: check if libusb drivers are properly installed" + ]) + else: + diagnostics["has_serial"] = False + diagnostics["serial_readable"] = False + diagnostics["issues"].append("No serial number in resource string") + + return diagnostics + + +def log_usb_diagnostics(resource_string: str) -> None: + """Log comprehensive USB diagnostics for a device.""" + diag = diagnose_usb_device(resource_string) + + logger.warning(f"USB Device Diagnostics for {resource_string}:") + logger.warning(f" Vendor ID: {diag['usb_info']['vendor_id'] if diag['usb_info'] else 'N/A'}") + logger.warning(f" Product ID: {diag['usb_info']['product_id'] if diag['usb_info'] else 'N/A'}") + logger.warning(f" Serial Number: {diag['usb_info']['serial_number'] if diag['usb_info'] else 'N/A'}") + logger.warning(f" Serial Readable: {diag['serial_readable']}") + + if diag['issues']: + logger.warning(" Issues detected:") + for issue in diag['issues']: + logger.warning(f" - {issue}") + + if diag['recommendations']: + logger.warning(" Recommendations:") + for rec in diag['recommendations']: + logger.warning(f" - {rec}") From 3760ca40e202da5315d6494576ec64c13d81e25b Mon Sep 17 00:00:00 2001 From: X9X0 Date: Mon, 15 Dec 2025 16:53:57 -0500 Subject: [PATCH 2/6] fix: Correct API request format and error handling for diagnostics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes two issues reported in PR #168: 1. USB Diagnostics API Issue: - Server endpoint now uses proper Pydantic request model (USBDiagnosticsRequest) - Client now sends resource_string in request body (json=...) instead of query params - Follows FastAPI best practices for POST request body handling 2. Pi Diagnostics Error Handling: - Fixed stderr decoding in ContainerError exception handler - Now properly handles both bytes and string stderr formats - Prevents errors when Docker container exits with non-zero code 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- client/api/client.py | 2 +- server/api/diagnostics.py | 10 +++++++++- server/api/equipment.py | 12 +++++++++--- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/client/api/client.py b/client/api/client.py index df67b1e..a321885 100644 --- a/client/api/client.py +++ b/client/api/client.py @@ -1421,7 +1421,7 @@ def run_usb_diagnostics(self, resource_string: str) -> Dict[str, Any]: """ response = self._session.post( f"{self.base_url}/equipment/diagnostics/usb", - params={"resource_string": resource_string}, + json={"resource_string": resource_string}, timeout=10 ) response.raise_for_status() diff --git a/server/api/diagnostics.py b/server/api/diagnostics.py index 470999b..c27cca2 100644 --- a/server/api/diagnostics.py +++ b/server/api/diagnostics.py @@ -433,7 +433,15 @@ def _run_diagnostic_on_host(script_path: str) -> dict: except docker.errors.ContainerError as e: # Container exited with non-zero code - output = e.stderr.decode('utf-8', errors='replace') if e.stderr else str(e) + # Handle both bytes and string stderr + if e.stderr: + if isinstance(e.stderr, bytes): + output = e.stderr.decode('utf-8', errors='replace') + else: + output = str(e.stderr) + else: + output = str(e) + return { "success": False, "output": output, diff --git a/server/api/equipment.py b/server/api/equipment.py index 473bb8c..152f6b4 100644 --- a/server/api/equipment.py +++ b/server/api/equipment.py @@ -74,8 +74,14 @@ async def discover_devices(): raise HTTPException(status_code=500, detail=str(e)) +class USBDiagnosticsRequest(BaseModel): + """Request to run USB diagnostics.""" + + resource_string: str + + @router.post("/diagnostics/usb") -async def run_usb_diagnostics(resource_string: str): +async def run_usb_diagnostics(request: USBDiagnosticsRequest): """ Run USB diagnostics on a device to troubleshoot connection issues. @@ -85,8 +91,8 @@ async def run_usb_diagnostics(resource_string: str): try: from server.utils.usb_diagnostics import diagnose_usb_device - diagnostics = diagnose_usb_device(resource_string) - logger.info(f"USB diagnostics run for {resource_string}") + diagnostics = diagnose_usb_device(request.resource_string) + logger.info(f"USB diagnostics run for {request.resource_string}") return diagnostics except Exception as e: From a06a346c040024a15627e5f0363a290ae38371e8 Mon Sep 17 00:00:00 2001 From: X9X0 Date: Mon, 15 Dec 2025 17:05:38 -0500 Subject: [PATCH 3/6] feat: Add git commit info to version endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhances the /system/version endpoint to include git commit information: - commit_hash: Short commit hash (e.g., "3760ca4") - commit_hash_full: Full commit hash - branch: Current git branch - commit_date: Commit date - commit_message: First line of commit message This makes it easy to verify what commit the server is running, which is helpful for debugging and ensuring the latest changes are deployed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- server/system/version.py | 113 +++++++++++++++++++++++++++++++++++---- 1 file changed, 103 insertions(+), 10 deletions(-) diff --git a/server/system/version.py b/server/system/version.py index 568c718..ad4d632 100644 --- a/server/system/version.py +++ b/server/system/version.py @@ -1,8 +1,12 @@ """Version management for LabLink.""" +import logging import re +import subprocess from pathlib import Path -from typing import Dict +from typing import Dict, Optional + +logger = logging.getLogger(__name__) # Read version from VERSION file _version_file = Path(__file__).parent.parent.parent / "VERSION" @@ -18,30 +22,119 @@ def get_version() -> str: return __version__ +def get_git_commit_info() -> Dict[str, Optional[str]]: + """Get git commit information for the running server. + + Returns: + Dictionary with git commit details: + - commit_hash: Short commit hash (e.g., "a1b2c3d") + - commit_hash_full: Full commit hash + - branch: Current git branch + - commit_date: Commit date + - commit_message: First line of commit message + """ + git_info = { + "commit_hash": None, + "commit_hash_full": None, + "branch": None, + "commit_date": None, + "commit_message": None, + } + + try: + # Get repository root (go up from server/system/) + repo_root = Path(__file__).parent.parent.parent + + # Get short commit hash + result = subprocess.run( + ["git", "rev-parse", "--short", "HEAD"], + cwd=repo_root, + capture_output=True, + text=True, + timeout=2, + ) + if result.returncode == 0: + git_info["commit_hash"] = result.stdout.strip() + + # Get full commit hash + result = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=repo_root, + capture_output=True, + text=True, + timeout=2, + ) + if result.returncode == 0: + git_info["commit_hash_full"] = result.stdout.strip() + + # Get branch name + result = subprocess.run( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + cwd=repo_root, + capture_output=True, + text=True, + timeout=2, + ) + if result.returncode == 0: + git_info["branch"] = result.stdout.strip() + + # Get commit date + result = subprocess.run( + ["git", "log", "-1", "--format=%ci"], + cwd=repo_root, + capture_output=True, + text=True, + timeout=2, + ) + if result.returncode == 0: + git_info["commit_date"] = result.stdout.strip() + + # Get commit message (first line) + result = subprocess.run( + ["git", "log", "-1", "--format=%s"], + cwd=repo_root, + capture_output=True, + text=True, + timeout=2, + ) + if result.returncode == 0: + git_info["commit_message"] = result.stdout.strip() + + except Exception as e: + logger.debug(f"Unable to get git commit info: {e}") + + return git_info + + def get_version_info() -> Dict[str, any]: """Get detailed version information. Returns: - Dictionary with version details including major, minor, patch + Dictionary with version details including major, minor, patch, and git info """ # Parse version string (format: major.minor.patch) match = re.match(r"(\d+)\.(\d+)\.(\d+)", __version__) if match: major, minor, patch = match.groups() - return { + version_info = { "version": __version__, "major": int(major), "minor": int(minor), "patch": int(patch), } + else: + # Fallback if version doesn't match expected format + version_info = { + "version": __version__, + "major": 0, + "minor": 0, + "patch": 0, + } - # Fallback if version doesn't match expected format - return { - "version": __version__, - "major": 0, - "minor": 0, - "patch": 0, - } + # Add git commit information + version_info["git"] = get_git_commit_info() + + return version_info def compare_versions(version1: str, version2: str) -> int: From bfb48a0868214f01ac8fccad2cb74ac1c0c038d1 Mon Sep 17 00:00:00 2001 From: X9X0 Date: Mon, 15 Dec 2025 17:10:23 -0500 Subject: [PATCH 4/6] feat: Add lablink-version CLI command for checking server version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a command-line utility to easily check the running server version and git commit information after SSH-ing to the server. Changes: - Created lablink-version.sh script that displays: - Version from VERSION file - Git branch, commit hash, and date - Latest commit message - Working directory status (clean or uncommitted changes) - Server running status (Docker or systemd) - Live API version query (if server is running) - Updated install-server.sh to: - Make lablink-version.sh executable during installation - Create /usr/local/bin/lablink-version symlink for easy access - Display the command in installation success message Usage after installation: ssh pi@your-server lablink-version 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- install-server.sh | 14 +++++++ lablink-version.sh | 92 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 lablink-version.sh diff --git a/install-server.sh b/install-server.sh index 3551792..5a3a35e 100755 --- a/install-server.sh +++ b/install-server.sh @@ -190,6 +190,17 @@ setup_environment() { else print_step ".env file already exists" fi + + # Make version check script executable + if [ -f "lablink-version.sh" ]; then + chmod +x lablink-version.sh + + # Create symlink in /usr/local/bin for easy access + if [ -w /usr/local/bin ] || [ -n "$SUDO" ]; then + $SUDO ln -sf "$LABLINK_DIR/lablink-version.sh" /usr/local/bin/lablink-version + print_step "Installed 'lablink-version' command" + fi + fi } deploy_with_docker() { @@ -313,6 +324,9 @@ print_success() { echo " Restart: sudo systemctl restart lablink.service" fi + echo "" + echo "Utility Commands:" + echo " Check version: lablink-version" echo "" echo "For help and documentation: https://docs.lablink.io" echo "" diff --git a/lablink-version.sh b/lablink-version.sh new file mode 100644 index 0000000..2ae7384 --- /dev/null +++ b/lablink-version.sh @@ -0,0 +1,92 @@ +#!/bin/bash +# LabLink Version Check Script +# Run this on the Raspberry Pi to check what version/commit is running + +set -e + +echo "======================================" +echo " LabLink Version Information" +echo "======================================" +echo "" + +# Get the LabLink directory +LABLINK_DIR="/opt/lablink" + +# If not in /opt/lablink, try to find it from current directory +if [ ! -d "$LABLINK_DIR" ]; then + # Try to find LabLink directory relative to script location + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + if [ -f "$SCRIPT_DIR/VERSION" ]; then + LABLINK_DIR="$SCRIPT_DIR" + else + echo "Error: LabLink directory not found" + echo "Expected at: /opt/lablink" + exit 1 + fi +fi + +cd "$LABLINK_DIR" + +# Get version from VERSION file +if [ -f "VERSION" ]; then + VERSION=$(cat VERSION) + echo "Version: $VERSION" +else + echo "Version: Unknown (VERSION file not found)" +fi + +echo "" + +# Get git information +if [ -d ".git" ]; then + echo "Git Information:" + echo " Branch: $(git rev-parse --abbrev-ref HEAD)" + echo " Commit: $(git rev-parse --short HEAD)" + echo " Date: $(git log -1 --format=%ci)" + echo "" + echo "Latest Commit:" + echo " $(git log -1 --oneline)" + echo "" + + # Check if there are uncommitted changes + if ! git diff-index --quiet HEAD --; then + echo "⚠️ WARNING: Uncommitted changes detected!" + git status --short + else + echo "✓ Working directory is clean" + fi +else + echo "Git Information: Not available (not a git repository)" +fi + +echo "" + +# Check if server is running +echo "Server Status:" +if command -v docker &> /dev/null; then + if docker ps --format '{{.Names}}' | grep -q 'lablink-server'; then + echo " ✓ Docker container 'lablink-server' is running" + + # Try to get version from API + if command -v curl &> /dev/null; then + echo "" + echo "Running Server Version (from API):" + RESPONSE=$(curl -s http://localhost:8000/api/system/version 2>/dev/null || echo "") + if [ -n "$RESPONSE" ]; then + # Extract version and git info using basic parsing + echo "$RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE" + else + echo " Unable to query API (server may not be responding)" + fi + fi + else + echo " ✗ Docker container 'lablink-server' is not running" + fi +elif systemctl is-active --quiet lablink-server 2>/dev/null; then + echo " ✓ LabLink server service is running" +else + echo " ✗ LabLink server is not running" +fi + +echo "" +echo "======================================" From d87706a4ba749ca9d602a787628e0144152d2741 Mon Sep 17 00:00:00 2001 From: X9X0 Date: Tue, 16 Dec 2025 10:26:32 -0500 Subject: [PATCH 5/6] fix: Support both SSH and Pi image deployments in lablink-version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lablink-version script now automatically detects the installation location by checking multiple common paths: 1. /opt/lablink (Pi image deployment via build-pi-image.sh) 2. $HOME/lablink (SSH deployment via install-server.sh) 3. Script directory (via symlink resolution) 4. Current directory (fallback) This ensures the command works correctly regardless of deployment method. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- lablink-version.sh | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/lablink-version.sh b/lablink-version.sh index 2ae7384..827c8e7 100644 --- a/lablink-version.sh +++ b/lablink-version.sh @@ -9,23 +9,47 @@ echo " LabLink Version Information" echo "======================================" echo "" -# Get the LabLink directory -LABLINK_DIR="/opt/lablink" +# Get the LabLink directory - try multiple locations +LABLINK_DIR="" -# If not in /opt/lablink, try to find it from current directory -if [ ! -d "$LABLINK_DIR" ]; then - # Try to find LabLink directory relative to script location +# Method 1: Check common deployment locations +for dir in "/opt/lablink" "$HOME/lablink" "$HOME/LabLink"; do + if [ -d "$dir" ] && [ -f "$dir/VERSION" ]; then + LABLINK_DIR="$dir" + break + fi +done + +# Method 2: If not found, try to find it relative to script location (for symlinked command) +if [ -z "$LABLINK_DIR" ]; then SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [ -f "$SCRIPT_DIR/VERSION" ]; then LABLINK_DIR="$SCRIPT_DIR" - else - echo "Error: LabLink directory not found" - echo "Expected at: /opt/lablink" - exit 1 fi fi +# Method 3: Check current directory as last resort +if [ -z "$LABLINK_DIR" ] && [ -f "$(pwd)/VERSION" ]; then + LABLINK_DIR="$(pwd)" +fi + +# Exit if LabLink directory not found +if [ -z "$LABLINK_DIR" ]; then + echo "Error: LabLink directory not found" + echo "" + echo "Searched locations:" + echo " - /opt/lablink (Pi image deployment)" + echo " - $HOME/lablink (SSH deployment)" + echo " - Script directory" + echo " - Current directory" + echo "" + echo "Please ensure you are running this on a system with LabLink installed." + exit 1 +fi + cd "$LABLINK_DIR" +echo "LabLink Directory: $LABLINK_DIR" +echo "" # Get version from VERSION file if [ -f "VERSION" ]; then From 7438bd57ca686ff7164200be62ed7bfa0ffc7c4a Mon Sep 17 00:00:00 2001 From: X9X0 Date: Tue, 16 Dec 2025 11:26:31 -0500 Subject: [PATCH 6/6] feat: Add lablink-version to help listings for all deployment methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates all help command listings to include the new lablink-version utility: Changes: - SSH deployment (ssh_deploy_wizard.py): - Added lablink-version to lablink-help() function output - Listed in "Status & Monitoring" section - Pi image deployment (build-pi-image.sh): - Added installation of lablink-version command during first boot setup - Updated lablink-status command listing - Updated setup completion message - Updated MOTD (message of the day) command listing Now when users run lablink-help or lablink-status, they will see: "lablink-version - Show version and git commit info" This ensures discoverability of the new version check command across both SSH and Pi image deployment methods. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- build-pi-image.sh | 9 +++++++++ client/ui/ssh_deploy_wizard.py | 1 + 2 files changed, 10 insertions(+) diff --git a/build-pi-image.sh b/build-pi-image.sh index 6079594..078958e 100755 --- a/build-pi-image.sh +++ b/build-pi-image.sh @@ -868,6 +868,7 @@ echo "════════════════════════ echo "" echo "Useful Commands:" echo " lablink-status - Show this status" +echo " lablink-version - Show version and git commit info" echo " lablink-start - Start LabLink" echo " lablink-stop - Stop LabLink" echo " lablink-restart - Restart LabLink" @@ -998,6 +999,12 @@ lablink-status UPDATESCRIPT chmod +x /usr/local/bin/lablink-update +# Install lablink-version command +if [ -f /opt/lablink/lablink-version.sh ]; then + cp /opt/lablink/lablink-version.sh /usr/local/bin/lablink-version + chmod +x /usr/local/bin/lablink-version +fi + echo "[LabLink] First boot setup complete!" echo "[LabLink] ════════════════════════════════════════════════════════" echo "[LabLink] " @@ -1007,6 +1014,7 @@ echo "[LabLink] Access LabLink at: http://$(hostname).local" echo "[LabLink] " echo "[LabLink] Useful commands:" echo "[LabLink] lablink-status - Check LabLink status" +echo "[LabLink] lablink-version - Show version and git commit info" echo "[LabLink] lablink-logs - View logs" echo "[LabLink] lablink-update - Update to latest code" echo "[LabLink] " @@ -1101,6 +1109,7 @@ fi echo "" echo "Quick Commands:" echo " lablink-status - Show detailed status" +echo " lablink-version - Show version and git commit info" echo " lablink-logs - View logs" echo " lablink-restart - Restart services" echo " lablink-update - Update to latest code" diff --git a/client/ui/ssh_deploy_wizard.py b/client/ui/ssh_deploy_wizard.py index 6a04c1f..a470ce3 100644 --- a/client/ui/ssh_deploy_wizard.py +++ b/client/ui/ssh_deploy_wizard.py @@ -718,6 +718,7 @@ def _install_convenience_commands(self, ssh, server_path, username): echo "" echo "Status & Monitoring:" echo " lablink-status - Show container status" + echo " lablink-version - Show version and git commit info" echo " lablink-logs - View all logs (follow mode)" echo " lablink-logs-server - View server logs only" echo " lablink-logs-web - View web dashboard logs only"