From 78d353614f174606bc316551939035501066877d Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 6 Feb 2026 23:34:30 -0800 Subject: [PATCH 1/2] ci(version-bump.yml): forcing release deployment auto release after making a new tag --- .github/workflows/version-bump.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 15e4b30..ed3bb13 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -95,9 +95,26 @@ jobs: grep "^version" pyproject.toml - name: Push changes + id: push run: | echo "=== Pushing commit and tags ===" # Push the version commit first git push origin main # Then push tags git push origin --tags + # Store the new version for triggering release (tag push with GITHUB_TOKEN doesn't trigger workflows) + NEW_TAG=$(git describe --tags --abbrev=0) + echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT + echo "version=${NEW_TAG#v}" >> $GITHUB_OUTPUT + + # Tag pushes made with GITHUB_TOKEN do not trigger other workflows (GitHub prevents + # recursive runs). So we explicitly dispatch the Release workflow via the API. + - name: Trigger Release workflow + run: | + VERSION="${{ steps.push.outputs.version }}" + echo "Triggering Release workflow for version: $VERSION" + curl -sS -X POST \ + -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + -H "Accept: application/vnd.github.v3+json" \ + "https://api.github.com/repos/${{ github.repository }}/actions/workflows/release.yml/dispatches" \ + -d "{\"ref\":\"main\",\"inputs\":{\"version\":\"$VERSION\"}}" From dae5dc16b86bc4b28d635a8d2cf83fceed0cab1d Mon Sep 17 00:00:00 2001 From: Sam Date: Sat, 7 Feb 2026 09:20:54 -0800 Subject: [PATCH 2/2] fix(status_poller.py): keep accurate state of the printer when idle and ready --- api/app.py | 22 ++++---- api/routes/__init__.py | 58 +++++++++++++++++++ api/services/status_poller.py | 22 +++++--- app/package-lock.json | 1 + app/src/hooks/useStats.ts | 15 +++++ app/src/routes/system.tsx | 102 +++++++++++++++++++++++++++++++++- build.py | 71 ++++++----------------- 7 files changed, 218 insertions(+), 73 deletions(-) diff --git a/api/app.py b/api/app.py index 4069c60..8e43e3b 100644 --- a/api/app.py +++ b/api/app.py @@ -23,12 +23,13 @@ file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) root_logger.addHandler(file_handler) -# Create console handler - will be updated with saved level after logger module imports -# Start with INFO as the safe default to prevent DEBUG spam during import -console_handler = logging.StreamHandler() -console_handler.setLevel(logging.INFO) -console_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) -root_logger.addHandler(console_handler) +# Console handler only when not packaged (frozen exe has no console window) +console_handler = None +if not getattr(sys, 'frozen', False): + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + console_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) + root_logger.addHandler(console_handler) # NOW import other modules (logging is already configured) import webbrowser @@ -48,10 +49,11 @@ # Import log level configuration and update console handler with saved level from utils.logger import get_console_log_level, LOG_LEVELS, DEFAULT_CONSOLE_LEVEL -# Update console handler with the saved log level (default INFO) -saved_level = LOG_LEVELS.get(get_console_log_level(), LOG_LEVELS[DEFAULT_CONSOLE_LEVEL]) -console_handler.setLevel(saved_level) -logging.info(f"Console log level set to: {get_console_log_level()}") +# Update console handler with the saved log level (when running with console) +if console_handler is not None: + saved_level = LOG_LEVELS.get(get_console_log_level(), LOG_LEVELS[DEFAULT_CONSOLE_LEVEL]) + console_handler.setLevel(saved_level) + logging.info(f"Console log level set to: {get_console_log_level()}") # Initialize the app with static and templates folders # Handle both development and packaged (PyInstaller) environments diff --git a/api/routes/__init__.py b/api/routes/__init__.py index 2bea526..1e274a7 100644 --- a/api/routes/__init__.py +++ b/api/routes/__init__.py @@ -318,7 +318,65 @@ def api_set_debug_flag(): }), 400 except Exception as e: return jsonify({'error': str(e)}), 500 + + # Log path and download for debugging (no console when running as standalone) + @app.route('/api/v1/system/logs/path', methods=['GET']) + def api_get_logs_path(): + """API: Get log directory and file paths for debugging""" + try: + log_dir = app.config.get('LOG_DIR', '') + if not log_dir: + log_dir = os.path.join(os.path.expanduser("~"), "PrintQueData") + app_log = os.path.join(log_dir, 'app.log') + logs_subdir = os.path.join(log_dir, 'logs') + return jsonify({ + 'log_dir': log_dir, + 'app_log': app_log, + 'logs_subdir': logs_subdir, + }) + except Exception as e: + return jsonify({'error': str(e)}), 500 + + @app.route('/api/v1/system/logs/download', methods=['GET']) + def api_download_logs(): + """API: Download recent logs as a text file (last 15 minutes)""" + try: + import io + from datetime import datetime + from utils.logger import get_recent_logs + + # Include main app log (PrintQueData/app.log) + logger's recent logs (PrintQueData/logs/*) + log_dir = app.config.get('LOG_DIR', '') + if not log_dir: + log_dir = os.path.join(os.path.expanduser("~"), "PrintQueData") + app_log = os.path.join(log_dir, 'app.log') + + parts = [] + if os.path.exists(app_log): + try: + with open(app_log, 'r', encoding='utf-8', errors='ignore') as f: + lines = f.readlines() + parts.append("=== MAIN APP LOG (app.log) - last 500 lines ===\n\n") + parts.extend(lines[-500:] if len(lines) > 500 else lines) + parts.append("\n\n") + except Exception as e: + parts.append(f"Error reading app.log: {e}\n\n") + + parts.append(get_recent_logs(minutes=15)) + + content = ''.join(parts) + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + filename = f'printque_logs_{timestamp}.txt' + + from flask import send_file + return send_file( + io.BytesIO(content.encode('utf-8')), + mimetype='text/plain', + as_attachment=True, + download_name=filename, + ) except Exception as e: + logging.error(f"Error in api_download_logs: {str(e)}") return jsonify({'error': str(e)}), 500 # Printer routes diff --git a/api/services/status_poller.py b/api/services/status_poller.py index 9461a19..1c90b5f 100644 --- a/api/services/status_poller.py +++ b/api/services/status_poller.py @@ -199,15 +199,21 @@ def update_bambu_printer_states(): printer['file'] = bambu_state['file'] # Handle state transitions + # Don't overwrite FINISHED with READY when Bambu reports IDLE after completion. + # Stay in FINISHED until user clicks "Mark Ready" or ejection completes. if new_state != current_state: - logging.info(f"Bambu {printer_name} state change: {current_state} -> {new_state}") - printer['state'] = new_state - printer['status'] = state_map.get(new_state, 'Unknown') - updates_made = True - - # Set finish_time when transitioning to FINISHED - if new_state == 'FINISHED' and current_state != 'FINISHED': - printer['finish_time'] = time.time() + if current_state == 'FINISHED' and new_state == 'READY': + logging.debug(f"Bambu {printer_name}: keeping FINISHED (ignore IDLE->READY until user marks ready)") + # Skip this transition - do not update state + else: + logging.info(f"Bambu {printer_name} state change: {current_state} -> {new_state}") + printer['state'] = new_state + printer['status'] = state_map.get(new_state, 'Unknown') + updates_made = True + + # Set finish_time when transitioning to FINISHED + if new_state == 'FINISHED' and current_state != 'FINISHED': + printer['finish_time'] = time.time() if updates_made: save_data(PRINTERS_FILE, PRINTERS) diff --git a/app/package-lock.json b/app/package-lock.json index 02de783..3007954 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "name": "app", + "license": "MIT", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", diff --git a/app/src/hooks/useStats.ts b/app/src/hooks/useStats.ts index 496e193..271618d 100644 --- a/app/src/hooks/useStats.ts +++ b/app/src/hooks/useStats.ts @@ -251,3 +251,18 @@ export function useSetDebugFlag() { }, }) } + +// Log paths for debugging (when app runs in background without console) +export interface LogsPath { + log_dir: string + app_log: string + logs_subdir: string +} + +export function useLogsPath() { + return useQuery({ + queryKey: ['system', 'logs', 'path'], + queryFn: () => api.get('/system/logs/path'), + staleTime: 60000, + }) +} diff --git a/app/src/routes/system.tsx b/app/src/routes/system.tsx index cab3900..b9f1f46 100644 --- a/app/src/routes/system.tsx +++ b/app/src/routes/system.tsx @@ -1,6 +1,17 @@ import { createFileRoute } from '@tanstack/react-router' -import { Bug, Clock, Cpu, HardDrive, Loader2, Server, Settings2 } from 'lucide-react' +import { + Bug, + Clock, + Copy, + Cpu, + FileText, + HardDrive, + Loader2, + Server, + Settings2, +} from 'lucide-react' import { toast } from 'sonner' +import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Label } from '@/components/ui/label' import { @@ -11,7 +22,13 @@ import { SelectValue, } from '@/components/ui/select' import { Switch } from '@/components/ui/switch' -import { useLoggingConfig, useSetDebugFlag, useSetLogLevel, useSystemInfo } from '@/hooks' +import { + useLoggingConfig, + useLogsPath, + useSetDebugFlag, + useSetLogLevel, + useSystemInfo, +} from '@/hooks' export const Route = createFileRoute('/system')({ component: SystemPage }) @@ -123,6 +140,9 @@ function SystemPage() { {/* Logging Settings */} + {/* Logs for debugging (when running without console) */} + + About PrintQue @@ -269,3 +289,81 @@ function LoggingSettings() { ) } + +// Logs for debugging when the app runs in the background (no console window) +function LogsForDebugging() { + const { data: logsPath, isLoading } = useLogsPath() + + const copyPath = (path: string) => { + navigator.clipboard.writeText(path) + toast.success('Path copied to clipboard') + } + + const downloadUrl = import.meta.env.DEV + ? 'http://localhost:5000/api/v1/system/logs/download' + : '/api/v1/system/logs/download' + + if (isLoading) { + return ( + + + + + Logs for debugging + + + +
+ +
+
+
+ ) + } + + return ( + + + + + Logs for debugging + + + When the app runs in the background (no console window), all logs are written to files. + Use these for troubleshooting. + + + +
+ +
+ + {logsPath?.log_dir ?? '—'} + + +
+
+

+ Main server log: app.log — Detailed logs:{' '} + logs/ folder (printque.log, + state_changes.log, etc.) +

+ +
+
+ ) +} diff --git a/build.py b/build.py index d237d29..a96ac7d 100644 --- a/build.py +++ b/build.py @@ -263,7 +263,7 @@ def create_pyinstaller_spec(): if IS_WINDOWS: exe_name = APP_NAME icon_file = "printque.ico" - console = True # Show console for debugging; set to False for release + console = False # No console window - standalone app opens browser only elif IS_MAC: exe_name = APP_NAME icon_file = "printque.icns" @@ -474,7 +474,7 @@ def create_distribution(): # Create distribution folder dist_folder.mkdir(parents=True, exist_ok=True) - # Copy executable + # Copy executable (single-file release; app uses %USERPROFILE%\PrintQueData for data/logs/uploads) if IS_WINDOWS: src_exe = DIST_DIR / f"{APP_NAME}.exe" if src_exe.exists(): @@ -488,48 +488,8 @@ def create_distribution(): if src_exe.exists(): shutil.copy(src_exe, dist_folder) - # Create data directories - for dirname in ["data", "uploads", "logs"]: - (dist_folder / dirname).mkdir(exist_ok=True) - - # Create launcher script - if IS_WINDOWS: - launcher = dist_folder / "Start_PrintQue.bat" - launcher.write_text(f'''@echo off -title {APP_NAME} Server -echo ================================================ -echo {APP_NAME} - Print Farm Manager -echo ================================================ -echo. -echo Starting {APP_NAME} server... -echo. -echo The web interface will be available at: -echo http://localhost:5000 -echo. -echo Press Ctrl+C to stop the server. -echo ================================================ -echo. -{APP_NAME}.exe -pause -''') - else: - launcher = dist_folder / f"start_{APP_NAME.lower()}.sh" - launcher.write_text(f'''#!/bin/bash -echo "================================================" -echo " {APP_NAME} - Print Farm Manager" -echo "================================================" -echo "" -echo "Starting {APP_NAME} server..." -echo "" -echo "The web interface will be available at:" -echo " http://localhost:5000" -echo "" -echo "Press Ctrl+C to stop the server." -echo "================================================" -echo "" -./{APP_NAME.lower() if IS_LINUX else APP_NAME + '.app/Contents/MacOS/' + APP_NAME} -''') - launcher.chmod(0o755) + # No data/logs/uploads folders in release - app uses user app data folder + # No .bat/.sh launcher - run the exe (or .app) directly # Create README readme = dist_folder / "README.txt" @@ -537,14 +497,19 @@ def create_distribution(): {'=' * 40} Quick Start: -1. {'Double-click Start_PrintQue.bat' if IS_WINDOWS else 'Run ./start_printque.sh'} -2. Open your browser to: http://localhost:5000 +1. {'Double-click ' + APP_NAME + '.exe (browser opens automatically).' if IS_WINDOWS else 'Run ./' + APP_NAME.lower() if IS_LINUX else 'Open ' + APP_NAME + '.app'} +2. Open http://localhost:5000 if it doesn't open automatically. 3. Add your printers and start managing! -Data Storage: -- Configuration: data/ -- Uploaded files: uploads/ -- Logs: logs/ +Data Storage (user app data folder, not next to the exe): +- Windows: %USERPROFILE%\\PrintQueData\\ +- macOS/Linux: ~/PrintQueData/ +- Config, uploads, and logs are stored there automatically. + +Code signing: +- The executable is not currently signed. On Windows you may see SmartScreen + or your antivirus flagging it; you can choose "Run anyway" or add an + exception. We plan to sign releases in the future. Open Source: - All features enabled, no printer limits @@ -552,7 +517,7 @@ def create_distribution(): - GitHub: https://github.com/PrintQue/PrintQue Support: -- Check logs/ for error details +- Logs: PrintQueData\\app.log (or printque.log in PrintQueData\\logs) - GitHub Issues: https://github.com/PrintQue/PrintQue/issues Version: {VERSION} @@ -632,12 +597,12 @@ def main(): dist_name = f"{APP_NAME}-{VERSION}-{platform_name}" if IS_WINDOWS: print(f" cd dist\\{dist_name}") - print(" Start_PrintQue.bat") + print(f" {APP_NAME}.exe") elif IS_MAC: print(f" open dist/{dist_name}/{APP_NAME}.app") else: print(f" cd dist/{dist_name}") - print(f" ./start_printque.sh") + print(f" ./{APP_NAME.lower()}") return 0