fix(labels): the manager's address, not the DX's — and one PDF, just opened

Three corrections from the first real session. Routed via a manager, the box
was prefilled with the DX's own address — the one thing that must not go on
that envelope. It now starts empty and the QRZ fetch fills in the MANAGER's;
switching the routing recomputes the box unless the operator has typed in it,
because a hand-checked address is not the app's to replace.

A fetch that came back with no street was overwriting a reviewed address with a
bare country — the cty.dat fallback dressed as an answer. It now refuses to
touch the box and says nothing was found. And the prefill no longer stacks the
same town three times (address + QTH + country all carrying it).

The output is ONE PDF for the whole session, each page at its own label size,
written to the temp dir and opened straight in the viewer — no save dialog: the
file is a print run, not a document to keep.
This commit is contained in:
2026-08-28 20:07:36 +02:00
parent 5ee0ade54b
commit 1d7633484b
7 changed files with 143 additions and 85 deletions
+28 -35
View File
@@ -14,13 +14,13 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"hamlog/internal/applog"
"hamlog/internal/pdf"
"hamlog/internal/qso"
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
)
// LabelPaperQueue returns the contacts whose paper QSL is REQUESTED or QUEUED
@@ -36,21 +36,30 @@ func (a *App) LabelPaperQueue() ([]qso.QSO, error) {
})
}
// LabelExportPDF writes one PDF of label pages and opens it in the system
// viewer, from which the operator prints. pages are base64 PNGs (data-URL
// prefix tolerated), all of the same wMm×hMm stock.
//
// Returns the chosen path ("" if the operator cancelled the dialog — not an
// error, they changed their mind).
func (a *App) LabelExportPDF(defaultName string, wMm, hMm float64, pages []string) (string, error) {
// LabelPDFPage is one page of the session's output: a rasterised label and its
// physical size. Sizes vary WITHIN one document — the operator asked for a
// single PDF holding QSO labels, addresses and return labels together, and PDF
// pages each carry their own MediaBox, so a 90×29 page can follow a 100×62 one.
type LabelPDFPage struct {
PNG string `json:"png"` // base64, data-URL prefix tolerated
WMm float64 `json:"w_mm"`
HMm float64 `json:"h_mm"`
}
// LabelOpenPDF writes the session's labels to ONE temporary PDF and opens it in
// the system viewer, from which the operator prints. No save dialog by choice:
// the file is a print run, not a document to keep — anyone who wants to keep it
// saves from the viewer.
func (a *App) LabelOpenPDF(pages []LabelPDFPage) (string, error) {
if len(pages) == 0 {
return "", fmt.Errorf("nothing to print")
}
if wMm < 5 || hMm < 5 || wMm > 400 || hMm > 400 {
return "", fmt.Errorf("label size out of range")
}
var doc pdf.Doc
for i, p := range pages {
for i, pg := range pages {
if pg.WMm < 5 || pg.HMm < 5 || pg.WMm > 400 || pg.HMm > 400 {
return "", fmt.Errorf("page %d: label size out of range", i+1)
}
p := pg.PNG
if idx := strings.Index(p, ","); idx >= 0 && strings.Contains(p[:idx], "base64") {
p = p[idx+1:]
}
@@ -58,7 +67,7 @@ func (a *App) LabelExportPDF(defaultName string, wMm, hMm float64, pages []strin
if err != nil {
return "", fmt.Errorf("page %d: %w", i+1, err)
}
if err := doc.AddImagePage(raw, wMm, hMm); err != nil {
if err := doc.AddImagePage(raw, pg.WMm, pg.HMm); err != nil {
return "", fmt.Errorf("page %d: %w", i+1, err)
}
}
@@ -66,32 +75,16 @@ func (a *App) LabelExportPDF(defaultName string, wMm, hMm float64, pages []strin
if err != nil {
return "", err
}
name := strings.TrimSpace(defaultName)
if name == "" {
name = "labels.pdf"
}
if !strings.HasSuffix(strings.ToLower(name), ".pdf") {
name += ".pdf"
}
path, err := wruntime.SaveFileDialog(a.ctx, wruntime.SaveDialogOptions{
DefaultFilename: name,
Title: "Save label PDF",
Filters: []wruntime.FileFilter{{DisplayName: "PDF", Pattern: "*.pdf"}},
})
if err != nil {
return "", err
}
if path == "" {
return "", nil // cancelled
}
// A timestamped name in the temp dir: two sessions in one evening must not
// fight over the file, least of all while a viewer holds the first one open.
path := filepath.Join(os.TempDir(), fmt.Sprintf("opslog-labels-%s.pdf", time.Now().Format("20060102-150405")))
if err := os.WriteFile(path, out, 0o644); err != nil {
return "", err
}
applog.Printf("labels: wrote %d page(s) (%.0f×%.0f mm) to %s", len(pages), wMm, hMm, path)
// Opened in the default PDF viewer — printing happens there, by design: the
// operator asked for a file they can check and print with their own tool.
applog.Printf("labels: wrote %d page(s) to %s", len(pages), path)
if err := exec.Command("rundll32", "url.dll,FileProtocolHandler", path).Start(); err != nil {
applog.Printf("labels: could not open the PDF viewer: %v", err)
return "", fmt.Errorf("the PDF was written to %s but no viewer opened: %w", path, err)
}
return path, nil
}