Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/version-bump.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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\"}}"
22 changes: 12 additions & 10 deletions api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
58 changes: 58 additions & 0 deletions api/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 14 additions & 8 deletions api/services/status_poller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions app/src/hooks/useStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<LogsPath>('/system/logs/path'),
staleTime: 60000,
})
}
102 changes: 100 additions & 2 deletions app/src/routes/system.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 })

Expand Down Expand Up @@ -123,6 +140,9 @@ function SystemPage() {
{/* Logging Settings */}
<LoggingSettings />

{/* Logs for debugging (when running without console) */}
<LogsForDebugging />

<Card>
<CardHeader>
<CardTitle>About PrintQue</CardTitle>
Expand Down Expand Up @@ -269,3 +289,81 @@ function LoggingSettings() {
</Card>
)
}

// 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 (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
Logs for debugging
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
</CardContent>
</Card>
)
}

return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
Logs for debugging
</CardTitle>
<CardDescription>
When the app runs in the background (no console window), all logs are written to files.
Use these for troubleshooting.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label className="text-muted-foreground">Log folder</Label>
<div className="flex items-center gap-2">
<code className="flex-1 rounded border bg-muted/50 px-3 py-2 text-sm break-all">
{logsPath?.log_dir ?? '—'}
</code>
<Button
type="button"
variant="outline"
size="icon"
onClick={() => logsPath && copyPath(logsPath.log_dir)}
title="Copy path"
>
<Copy className="h-4 w-4" />
</Button>
</div>
</div>
<p className="text-xs text-muted-foreground">
Main server log: <code className="rounded bg-muted px-1">app.log</code> — Detailed logs:{' '}
<code className="rounded bg-muted px-1">logs/</code> folder (printque.log,
state_changes.log, etc.)
</p>
<div className="flex flex-wrap gap-2">
<Button variant="default" asChild>
<a href={downloadUrl} download target="_blank" rel="noopener noreferrer">
Download recent logs (last 15 min)
</a>
</Button>
</div>
</CardContent>
</Card>
)
}
Loading