From d72b3d5a9e9218f6eff9b99c19b9f9c4e88c8fea Mon Sep 17 00:00:00 2001 From: Piotr Tabor Date: Fri, 12 Jun 2026 12:21:13 +0200 Subject: [PATCH 1/5] feat: set custom filename on PDF download --- containers/backend/main.py | 13 ++++++++++--- editor/index.html | 7 +++++-- src/html/templates/songbook_edit/songbook_edit.js | 5 ++++- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/containers/backend/main.py b/containers/backend/main.py index 543a7a2b..5ca053ec 100644 --- a/containers/backend/main.py +++ b/containers/backend/main.py @@ -389,7 +389,11 @@ async def get_job_status(job_id: str): raise HTTPException(status_code=500, detail=f"Failed to check storage: {e}") @app.get("/api/jobs/{job_id}/download") -async def download_job(job_id: str): +async def download_job(job_id: str, filename: Optional[str] = None): + dl_filename = filename if filename else f"{job_id}.pdf" + if not dl_filename.endswith(".pdf"): + dl_filename += ".pdf" + if STORAGE_URI.startswith("gs://"): bucket_name = STORAGE_URI[5:] storage_client = storage.Client() @@ -398,13 +402,16 @@ async def download_job(job_id: str): if blob.exists(): pdf_bytes = blob.download_as_bytes() from fastapi import Response - return Response(content=pdf_bytes, media_type="application/pdf", headers={"Content-Disposition": f"attachment; filename={job_id}.pdf"}) + import urllib.parse + encoded_filename = urllib.parse.quote(dl_filename) + headers = {"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"} + return Response(content=pdf_bytes, media_type="application/pdf", headers=headers) else: local_dir = STORAGE_URI if STORAGE_URI.startswith("file://"): local_dir = STORAGE_URI[7:] pdf_path = os.path.join(local_dir, f"{job_id}.pdf") if os.path.exists(pdf_path): - return FileResponse(pdf_path, media_type="application/pdf", filename=f"{job_id}.pdf") + return FileResponse(pdf_path, media_type="application/pdf", filename=dl_filename) raise HTTPException(status_code=404, detail="File not found") diff --git a/editor/index.html b/editor/index.html index b7ffc88b..e9e8cd03 100644 --- a/editor/index.html +++ b/editor/index.html @@ -256,8 +256,11 @@

Musisz się zalogować

() => notice("Generowanie PDF... Proszę czekać (może potrwać do kilkunastu sekund)", songbook), (url) => { notice("PDF wygenerowany!", songbook); - if (pdfWindow) pdfWindow.location.href = url; - else window.open(url, '_blank'); + const dateStr = new Date().toISOString().split('T')[0]; + const filename = encodeURIComponent(`${title || 'piosenka'}_${dateStr}.pdf`); + const finalUrl = `${url}?filename=${filename}`; + if (pdfWindow) pdfWindow.location.href = finalUrl; + else window.open(finalUrl, '_blank'); }, (err, errUrl) => { alert(err); diff --git a/src/html/templates/songbook_edit/songbook_edit.js b/src/html/templates/songbook_edit/songbook_edit.js index 09f2d972..f6e15e35 100644 --- a/src/html/templates/songbook_edit/songbook_edit.js +++ b/src/html/templates/songbook_edit/songbook_edit.js @@ -673,7 +673,10 @@ async function renderPDF() { } if (pdfStatusText) pdfStatusText.style.display = 'none'; if (pdfDownloadLink) { - pdfDownloadLink.href = url; + const songbookTitle = document.getElementById('songbookTitle').value.trim() || 'spiewnik'; + const dateStr = new Date().toISOString().split('T')[0]; + const filename = encodeURIComponent(`${songbookTitle}_${dateStr}.pdf`); + pdfDownloadLink.href = `${url}?filename=${filename}`; pdfDownloadLink.style.display = 'block'; } }, From 0bc4397accb5ab02792d72170b08e70917d2198f Mon Sep 17 00:00:00 2001 From: Piotr Tabor Date: Fri, 12 Jun 2026 17:51:40 +0200 Subject: [PATCH 2/5] fix: handle github rate limit 500 error gracefully by using cached zip --- containers/backend/main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/containers/backend/main.py b/containers/backend/main.py index 5ca053ec..611eaed7 100644 --- a/containers/backend/main.py +++ b/containers/backend/main.py @@ -69,7 +69,10 @@ def _fetch_branch_archive(ref: str, warmup: bool = False) -> str: new_etag = response.headers.get("ETag") except Exception as e: if not warmup: - raise HTTPException(status_code=500, detail=f"Failed to check GitHub archive status: {e}") + if current_etag and os.path.exists(ref_cache_dir): + new_etag = current_etag + else: + raise HTTPException(status_code=500, detail=f"Failed to check GitHub archive status: {e}") else: return "" From 842e7e2210dc05d1a522ce72c4f3e34249bcfa13 Mon Sep 17 00:00:00 2001 From: Piotr Tabor Date: Fri, 12 Jun 2026 17:53:34 +0200 Subject: [PATCH 3/5] feat: inline pdf viewer UX for single song editor and inline disposition --- containers/backend/main.py | 7 ++-- editor/index.html | 39 ++++++++++++------- .../templates/songbook_edit/songbook_edit.js | 2 +- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/containers/backend/main.py b/containers/backend/main.py index 611eaed7..ec922ece 100644 --- a/containers/backend/main.py +++ b/containers/backend/main.py @@ -392,7 +392,7 @@ async def get_job_status(job_id: str): raise HTTPException(status_code=500, detail=f"Failed to check storage: {e}") @app.get("/api/jobs/{job_id}/download") -async def download_job(job_id: str, filename: Optional[str] = None): +async def download_job(job_id: str, filename: Optional[str] = None, disposition: str = "inline"): dl_filename = filename if filename else f"{job_id}.pdf" if not dl_filename.endswith(".pdf"): dl_filename += ".pdf" @@ -407,7 +407,7 @@ async def download_job(job_id: str, filename: Optional[str] = None): from fastapi import Response import urllib.parse encoded_filename = urllib.parse.quote(dl_filename) - headers = {"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"} + headers = {"Content-Disposition": f"{disposition}; filename*=UTF-8''{encoded_filename}"} return Response(content=pdf_bytes, media_type="application/pdf", headers=headers) else: local_dir = STORAGE_URI @@ -415,6 +415,7 @@ async def download_job(job_id: str, filename: Optional[str] = None): local_dir = STORAGE_URI[7:] pdf_path = os.path.join(local_dir, f"{job_id}.pdf") if os.path.exists(pdf_path): - return FileResponse(pdf_path, media_type="application/pdf", filename=dl_filename) + headers = {"Content-Disposition": f"{disposition}; filename=\"{dl_filename}\""} + return FileResponse(pdf_path, media_type="application/pdf", filename=dl_filename, headers=headers) raise HTTPException(status_code=404, detail="File not found") diff --git a/editor/index.html b/editor/index.html index e9e8cd03..c6624c99 100644 --- a/editor/index.html +++ b/editor/index.html @@ -244,28 +244,41 @@

Musisz się zalogować

branch: branch || "main" }; - // Open blank window immediately to satisfy popup blocker - const pdfWindow = window.open('about:blank', '_blank'); - if (pdfWindow) { - pdfWindow.document.write("
Trwa generowanie PDF... Proszę czekać (zwykle około 10-20 sekund).
"); - } - renderPdfCloudRun( payload, "/api/render/song_xml", () => notice("Generowanie PDF... Proszę czekać (może potrwać do kilkunastu sekund)", songbook), (url) => { - notice("PDF wygenerowany!", songbook); const dateStr = new Date().toISOString().split('T')[0]; const filename = encodeURIComponent(`${title || 'piosenka'}_${dateStr}.pdf`); - const finalUrl = `${url}?filename=${filename}`; - if (pdfWindow) pdfWindow.location.href = finalUrl; - else window.open(finalUrl, '_blank'); + const finalUrl = `${url}?filename=${filename}&disposition=inline`; + + const noticeDiv = document.createElement("div"); + noticeDiv.classList.add("notice"); + noticeDiv.innerHTML = ` +
+

PDF wygenerowany!

+ Gotowe! Otwórz Podgląd PDF +
+ Zamknij to okienko +
+
+ `; + document.body.appendChild(noticeDiv); }, (err, errUrl) => { - alert(err); - if (pdfWindow && errUrl) pdfWindow.location.href = errUrl; - else if (pdfWindow) pdfWindow.close(); + alert("Błąd generowania: " + err); } ); } diff --git a/src/html/templates/songbook_edit/songbook_edit.js b/src/html/templates/songbook_edit/songbook_edit.js index f6e15e35..aa850e53 100644 --- a/src/html/templates/songbook_edit/songbook_edit.js +++ b/src/html/templates/songbook_edit/songbook_edit.js @@ -676,7 +676,7 @@ async function renderPDF() { const songbookTitle = document.getElementById('songbookTitle').value.trim() || 'spiewnik'; const dateStr = new Date().toISOString().split('T')[0]; const filename = encodeURIComponent(`${songbookTitle}_${dateStr}.pdf`); - pdfDownloadLink.href = `${url}?filename=${filename}`; + pdfDownloadLink.href = `${url}?filename=${filename}&disposition=inline`; pdfDownloadLink.style.display = 'block'; } }, From 4db5eb06025fb97bf76e032a7eaf6f89ec23c3f3 Mon Sep 17 00:00:00 2001 From: Piotr Tabor Date: Fri, 12 Jun 2026 17:57:12 +0200 Subject: [PATCH 4/5] fix: fallback to main branch if requested branch returns 404 --- containers/backend/main.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/containers/backend/main.py b/containers/backend/main.py index ec922ece..7f9007ad 100644 --- a/containers/backend/main.py +++ b/containers/backend/main.py @@ -71,6 +71,8 @@ def _fetch_branch_archive(ref: str, warmup: bool = False) -> str: if not warmup: if current_etag and os.path.exists(ref_cache_dir): new_etag = current_etag + elif isinstance(e, urllib.error.HTTPError) and e.code == 404: + raise HTTPException(status_code=404, detail=f"GitHub archive not found for branch '{ref}'") else: raise HTTPException(status_code=500, detail=f"Failed to check GitHub archive status: {e}") else: @@ -145,7 +147,15 @@ def overlay_branch_data(ref: str, work_dir: str, warmup: bool = False): Security: only data directories are copied. No executable code (src/, *.sh, *.py) from the branch is ever used. """ - cache_dir = _fetch_branch_archive(ref, warmup=warmup) + try: + cache_dir = _fetch_branch_archive(ref, warmup=warmup) + except HTTPException as e: + if e.status_code == 404 and ref != "main": + print(f"Branch {ref} not found on upstream, falling back to 'main' branch") + cache_dir = _fetch_branch_archive("main", warmup=warmup) + else: + raise e + if not cache_dir: return From e4f790556e6b7627afc4dc076574e485f100de22 Mon Sep 17 00:00:00 2001 From: Piotr Tabor Date: Fri, 12 Jun 2026 18:01:31 +0200 Subject: [PATCH 5/5] perf: remove unnecessary branch archive fetching for single song pdf render --- containers/backend/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/containers/backend/main.py b/containers/backend/main.py index 7f9007ad..73564515 100644 --- a/containers/backend/main.py +++ b/containers/backend/main.py @@ -303,7 +303,9 @@ async def render_song_xml(request: XmlRequest, background_tasks: BackgroundTasks job_id = f"job_{uuid.uuid4().hex[:8]}" temp_dir = tempfile.mkdtemp(prefix="songbook_") work_dir = setup_work_dir(temp_dir) - overlay_branch_data(request.branch, work_dir) + # We do not overlay branch data for a single song render + # because the song is fully provided in xml_content + # and all LaTeX templates are already bundled in the Docker image. xml_path = os.path.join(work_dir, "custom.xml") with open(xml_path, "w", encoding="utf-8") as f: