From a88b83fddb1603f32cc26a1495a3bfb60ab380b9 Mon Sep 17 00:00:00 2001 From: Robert Romero Date: Fri, 8 Aug 2025 18:51:20 -0700 Subject: [PATCH] Improve invoice export error handling --- src/invoice.py | 27 ++++++++++++++++++++------- src/slurmcostmanager.js | 36 +++++++++++++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/src/invoice.py b/src/invoice.py index dbde63f..271c3d6 100755 --- a/src/invoice.py +++ b/src/invoice.py @@ -13,12 +13,23 @@ import sys from datetime import datetime -from reportlab.lib import colors -from reportlab.lib.enums import TA_RIGHT -from reportlab.lib.pagesizes import LETTER -from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet -from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, Table, - TableStyle) +# Reportlab is required for PDF generation. If it's missing, emit a clear +# error message on stderr so callers can surface the failure to users. +try: + from reportlab.lib import colors + from reportlab.lib.enums import TA_RIGHT + from reportlab.lib.pagesizes import LETTER + from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet + from reportlab.platypus import ( + SimpleDocTemplate, + Paragraph, + Spacer, + Table, + TableStyle, + ) +except ModuleNotFoundError as exc: # pragma: no cover - exercised via JS + print(str(exc), file=sys.stderr) + sys.exit(1) def _load_profile(base_dir): @@ -174,8 +185,10 @@ def main(): buffer = io.BytesIO() generate_invoice(buffer, invoice_data) - pdf_bytes = buffer.getvalue() + buffer.seek(0) + pdf_bytes = buffer.read() sys.stdout.write(base64.b64encode(pdf_bytes).decode("ascii")) + sys.stdout.flush() if __name__ == "__main__": diff --git a/src/slurmcostmanager.js b/src/slurmcostmanager.js index 438e908..b48035c 100644 --- a/src/slurmcostmanager.js +++ b/src/slurmcostmanager.js @@ -789,6 +789,7 @@ function Details({ account: '', user: '' }); + const [error, setError] = useState(null); function toggle(account) { setExpanded(prev => (prev === account ? null : account)); @@ -851,7 +852,10 @@ function Details({ const a = document.createElement('a'); a.href = url; a.download = 'details.csv'; + // Append link to DOM so browsers will download the file + document.body.appendChild(a); a.click(); + document.body.removeChild(a); URL.revokeObjectURL(url); } @@ -900,11 +904,27 @@ function Details({ 'Thank you for your prompt payment. For questions regarding this invoice, please contact our office.' }; try { + setError(null); + if (!filteredDetails.length) { + setError('No usage data matches current filters'); + return; + } const output = await window.cockpit.spawn( ['python3', `${PLUGIN_BASE}/invoice.py`], - { input: JSON.stringify(invoiceData), err: 'message' } + { input: JSON.stringify(invoiceData), err: 'out' } ); - const byteChars = atob(output.trim()); + const trimmed = output.trim(); + if (!trimmed) { + setError('Invoice generation returned no data'); + return; + } + let byteChars; + try { + byteChars = atob(trimmed); + } catch (decodeErr) { + setError(trimmed || decodeErr.message || String(decodeErr)); + return; + } const byteNumbers = new Array(byteChars.length); for (let i = 0; i < byteChars.length; i++) { byteNumbers[i] = byteChars.charCodeAt(i); @@ -916,10 +936,14 @@ function Details({ const a = document.createElement('a'); a.href = url; a.download = 'recharge_invoice.pdf'; + // Append link to DOM so browsers will download the file + document.body.appendChild(a); a.click(); + document.body.removeChild(a); URL.revokeObjectURL(url); } catch (e) { console.error(e); + setError(e.message || String(e)); } } @@ -957,7 +981,13 @@ function Details({ ); }), React.createElement('button', { onClick: exportCSV }, 'Export CSV'), - React.createElement('button', { onClick: exportInvoice }, 'Export Invoice') + React.createElement('button', { onClick: exportInvoice }, 'Export Invoice'), + error && + React.createElement( + 'span', + { className: 'error', style: { marginLeft: '0.5em' } }, + error + ) ), React.createElement( 'div',