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
9 changes: 6 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
name: Release Build

# Triggered automatically when a version tag is pushed (e.g. by Version Bump workflow).
# Manual run: use the "Run workflow" button in the Actions tab (requires write access).
# Note: Triggering workflow_dispatch via API (e.g. gh workflow run) requires a PAT with
# "workflow" scope; the default GITHUB_TOKEN cannot trigger workflows (403).
on:
push:
tags:
- 'v*.*.*'
# Manual trigger from GitHub Actions UI
workflow_dispatch:
inputs:
version:
Expand Down Expand Up @@ -175,9 +178,9 @@ jobs:
1. Download the ZIP file for your platform
2. Extract to a folder
3. Run the executable:
- **Windows**: Double-click `Start_PrintQue.bat`
- **Windows**: Double-click `PrintQue.exe`
- **macOS**: Open `PrintQue.app`
- **Linux**: Run `./start_printque.sh`
- **Linux**: Run `./printque`
4. Open http://localhost:5000 in your browser

files: |
Expand Down
12 changes: 8 additions & 4 deletions .github/workflows/version-bump.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,21 +100,25 @@ jobs:
echo "=== Pushing commit and tags ==="
# Push the version commit first
git push origin main
# Then push tags
# Then push tags (pushes from Actions do not trigger other workflows, so we trigger Release below)
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.
# GITHUB_TOKEN cannot trigger other workflows (403). Per GitHub docs, use a PAT stored as a repo secret.
# Create a PAT with "workflow" scope → Settings → Secrets → Actions → New secret (e.g. name: RELEASE_DISPATCH_TOKEN).
- name: Trigger Release workflow
run: |
VERSION="${{ steps.push.outputs.version }}"
if [ -z "$VERSION" ]; then
echo "Could not determine version, skipping release trigger"
exit 0
fi
echo "Triggering Release workflow for version: $VERSION"
curl -sS -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Authorization: token ${{ secrets.RELEASE_DISPATCH_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\"}}"
28 changes: 28 additions & 0 deletions api/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,27 @@ def api_set_debug_flag():
except Exception as e:
return jsonify({'error': str(e)}), 500

@app.route('/api/v1/system/shutdown', methods=['POST'])
def api_shutdown():
"""API: Shut down the PrintQue server (for background/standalone mode with no console)"""
try:
import threading
import time

def exit_after_delay():
time.sleep(2)
logging.info("Shutdown requested from web UI - exiting.")
os._exit(0) # Force process exit from any thread

threading.Thread(target=exit_after_delay, daemon=True).start()
return jsonify({
'success': True,
'message': 'PrintQue is shutting down. Start it again from the executable or Start_PrintQue.bat when needed.',
})
except Exception as e:
logging.error(f"Error in api_shutdown: {str(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():
Expand All @@ -335,6 +356,7 @@ def api_get_logs_path():
'logs_subdir': logs_subdir,
})
except Exception as e:
logging.error(f"Error in api_get_logs_path: {str(e)}")
return jsonify({'error': str(e)}), 500

@app.route('/api/v1/system/logs/download', methods=['GET'])
Expand Down Expand Up @@ -516,14 +538,20 @@ def api_mark_printer_ready(printer_name):
for printer in PRINTERS:
if printer['name'] == printer_name:
if printer['state'] in ['FINISHED', 'EJECTING', 'COOLING']:
import time
printer['state'] = 'READY'
printer['status'] = 'Ready'
printer['manually_set'] = True
printer['manual_timeout'] = time.time() + 3600 # 1 hour protection
printer['progress'] = 0
printer['time_remaining'] = 0
printer['file'] = None
printer['job_id'] = None
printer['order_id'] = None
printer['ejection_processed'] = False
printer['ejection_in_progress'] = False
printer['ejection_start_time'] = None
printer['finish_time'] = None
# Clear cooldown state if skipping cooldown
printer['cooldown_target_temp'] = None
printer['cooldown_order_id'] = None
Expand Down
11 changes: 11 additions & 0 deletions api/services/status_poller.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,17 @@ def update_bambu_printer_states():
if current_state == 'COOLING':
continue

# Skip state updates for manually set printers (e.g., user clicked Mark Ready)
# The manually_set flag protects the READY state from being overwritten by MQTT
if printer.get('manually_set', False) and current_state == 'READY':
logging.debug(f"Bambu {printer_name}: skipping MQTT state update - printer is manually set to READY")
# Still update temperatures even when preserving manual state
if 'nozzle_temp' in bambu_state:
printer['nozzle_temp'] = bambu_state['nozzle_temp']
if 'bed_temp' in bambu_state:
printer['bed_temp'] = bambu_state['bed_temp']
continue

# Get the new state from Bambu MQTT
new_state = bambu_state.get('state', current_state)

Expand Down
6 changes: 6 additions & 0 deletions app/src/hooks/useStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,12 @@ export function useSetDebugFlag() {
})
}

export function useShutdown() {
return useMutation({
mutationFn: () => api.post<ApiResponse & { message?: string }>('/system/shutdown'),
})
}

// Log paths for debugging (when app runs in background without console)
export interface LogsPath {
log_dir: string
Expand Down
86 changes: 86 additions & 0 deletions app/src/routes/system.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,22 @@ import {
FileText,
HardDrive,
Loader2,
Power,
Server,
Settings2,
} from 'lucide-react'
import { useState } from 'react'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Label } from '@/components/ui/label'
import {
Select,
Expand All @@ -27,6 +37,7 @@ import {
useLogsPath,
useSetDebugFlag,
useSetLogLevel,
useShutdown,
useSystemInfo,
} from '@/hooks'

Expand Down Expand Up @@ -143,6 +154,9 @@ function SystemPage() {
{/* Logs for debugging (when running without console) */}
<LogsForDebugging />

{/* Shut down (when running in background with no console) */}
<ShutDownCard />

<Card>
<CardHeader>
<CardTitle>About PrintQue</CardTitle>
Expand Down Expand Up @@ -367,3 +381,75 @@ function LogsForDebugging() {
</Card>
)
}

// Shut down PrintQue from the browser when running in background (no console)
function ShutDownCard() {
const [confirmOpen, setConfirmOpen] = useState(false)
const shutdown = useShutdown()

const handleShutdown = async () => {
try {
await shutdown.mutateAsync()
toast.success(
'PrintQue is shutting down. Start it again from the executable or Start_PrintQue.bat when needed.'
)
setConfirmOpen(false)
// Page will stop loading when server exits
} catch {
toast.error('Failed to shut down')
}
}

return (
<>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Power className="h-5 w-5" />
Shut down PrintQue
</CardTitle>
<CardDescription>
When the app runs in the background (no console window), you can stop it from here. You
can start it again by running PrintQue.exe or Start_PrintQue.bat.
</CardDescription>
</CardHeader>
<CardContent>
<Button
variant="destructive"
onClick={() => setConfirmOpen(true)}
disabled={shutdown.isPending}
>
{shutdown.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Shutting down…
</>
) : (
'Shut down PrintQue'
)}
</Button>
</CardContent>
</Card>

<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Shut down PrintQue?</DialogTitle>
<DialogDescription>
The server will stop and this page will no longer load. To use PrintQue again, run
PrintQue.exe or Start_PrintQue.bat.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setConfirmOpen(false)}>
Cancel
</Button>
<Button variant="destructive" onClick={handleShutdown} disabled={shutdown.isPending}>
{shutdown.isPending ? 'Shutting down…' : 'Shut down'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"description": "PrintQue - Open Source Print Farm Manager",
"scripts": {
"commit": "cz",
"prepare": "husky"
"prepare": "husky",
"dev": "npm run dev --prefix app",
"api": "cd api && python app.py"
},
"config": {
"commitizen": {
Expand Down