feat: WiX ProductVersion from HelperVersion + build-tray-msi + CERT live QR

- build-tray-msi.ps1 candle/light or tip; version from common.ps1
- certificate.html final QR via qrcode CDN when digest present
This commit is contained in:
George Lambert 2026-10-13 23:08:55 -04:00
commit f766e537ba
6 changed files with 299 additions and 19 deletions

View file

@ -14,11 +14,11 @@ Repo now includes **authoring/docs scaffolds** for the two commercial gaps that
| Scaffold | Path | What it is | What it is not |
|----------|------|------------|----------------|
| **WiX tray presence MSI skeleton** | [`platform/windows/wix/`](../platform/windows/wix/) — `VeraeTrayShortcuts.wxs`, `VeraeTrayShortcutsFragment.wxs`, [`README.md`](../platform/windows/wix/README.md) | Non-elevated **per-user** WiX v3 product + shortcut fragment: stages `verae-tray.ps1`, Start Menu `Programs\Verae\Verae Tray.lnk`, Startup `Verae Tray.lnk`. Build when **candle/light** (or adapted WiX v4) are present. | Full elevated commercial MSI (printers, Program Files, Authenticode, NSIS parity, CI-signed MSI in `dist/`). No MSI binary is produced in-repo without the WiX toolset. |
| **WiX tray presence MSI skeleton** | [`platform/windows/wix/`](../platform/windows/wix/) — `VeraeTrayShortcuts.wxs`, `VeraeTrayShortcutsFragment.wxs`, [`README.md`](../platform/windows/wix/README.md); driver [`scripts/build-tray-msi.ps1`](../platform/windows/scripts/build-tray-msi.ps1) | Non-elevated **per-user** WiX v3 product + shortcut fragment: stages `verae-tray.ps1`, Start Menu `Programs\Verae\Verae Tray.lnk`, Startup `Verae Tray.lnk`. **ProductVersion** wired from `Get-VeraePackageVersion` → `internal/job.HelperVersion` (or `VERSION=` / `-Version`). `build-tray-msi.ps1` runs **candle/light** when present; otherwise prints WiX install tip (exit 0). | Full elevated commercial MSI (printers, Program Files, Authenticode, NSIS parity, CI-signed MSI in `dist/`). No MSI binary without the WiX toolset on the build host. |
| **Menubar notarize checklist** | [`platform/macos/menubar/NOTARIZE-CHECKLIST.md`](../platform/macos/menubar/NOTARIZE-CHECKLIST.md) | Operator step list: unsigned build → codesign hardened runtime → `ditto` zip → `notarytool` (keychain profile **or** API key placeholders) → staple → `spctl`. No secrets. | Automated sign/notarize in `build-menubar-app.sh`, stapled `.app.zip`/DMG release, or CI keychain import. |
**Still use for day-to-day presence (no WiX / no Apple certs):**
Windows `scripts/build-tray-startmenu.ps1` + `install-tray-startup.ps1`; macOS `verae-print menubar --install` / unsigned `build-menubar-app.sh`. Full product install remains NSIS (`installer-windows.nsi`) and macOS `pkg/build-pkg.sh`.
Windows `scripts/build-tray-startmenu.ps1` + `install-tray-startup.ps1`; macOS `verae-print menubar --install` / unsigned `build-menubar-app.sh`. Full product install remains NSIS (`installer-windows.nsi`) and macOS `pkg/build-pkg.sh`. Tray MSI skeleton build: `scripts/build-tray-msi.ps1` (optional WiX).
---
@ -47,7 +47,7 @@ Related but out of scope for “done” above (still backlog-adjacent): Linux `.
| Tray NotifyIcon + GET `/status` | [`platform/windows/scripts/verae-tray.ps1`](../platform/windows/scripts/verae-tray.ps1) | User-level; see [PACKAGING.md](PACKAGING.md) § Windows tray |
| Startup-folder tray install | [`platform/windows/scripts/install-tray-startup.ps1`](../platform/windows/scripts/install-tray-startup.ps1) | **Not** MSI; no elevation |
| WiX-free Start Menu stub | [`platform/windows/scripts/build-tray-startmenu.ps1`](../platform/windows/scripts/build-tray-startmenu.ps1) | Explicitly “future MSI can supersede” |
| **WiX tray presence skeleton** (per-user, non-elevated) | [`platform/windows/wix/`](../platform/windows/wix/) (`VeraeTrayShortcuts.wxs` + fragment + README) | **Scaffold shipped** — Start Menu + Startup → `verae-tray.ps1` only; needs candle/light to emit MSI |
| **WiX tray presence skeleton** (per-user, non-elevated) | [`platform/windows/wix/`](../platform/windows/wix/) + [`scripts/build-tray-msi.ps1`](../platform/windows/scripts/build-tray-msi.ps1) | **Scaffold shipped** — ProductVersion ← HelperVersion; candle/light when present, else WiX tip; not full product MSI |
| Shared version helper | [`platform/windows/scripts/common.ps1`](../platform/windows/scripts/common.ps1) `Get-VeraePackageVersion` | Aligns tray scripts with `HelperVersion` |
| End-user Windows readme | [`platform/windows/README-WINDOWS.txt`](../platform/windows/README-WINDOWS.txt), [`README.txt`](../platform/windows/README.txt) | Documents NSIS flow; states **no elevated MSI** for tray |
| Packaging policy doc | [PACKAGING.md](PACKAGING.md) | “**No elevated MSI**” for tray presence by design |
@ -146,7 +146,7 @@ NSIS product already covers much of the *install experience* (files under Progra
|-------------|--------|------------------|
| Windows tray / Startup / Start Menu (user) | Yes | Yes for **presence**; not enterprise MSI |
| Windows NSIS `setup.exe` | Yes | **Partial** — full GUI install; needs Authenticode + version align; not MSI |
| Windows **MSI** (WiX) tray presence skeleton | **Yes** (`.wxs` + README under `platform/windows/wix/`) | **No** — scaffold only; needs toolset build + is not full product MSI |
| Windows **MSI** (WiX) tray presence skeleton | **Yes** (`.wxs` + `build-tray-msi.ps1`; ProductVersion ← HelperVersion) | **No** — scaffold only; MSI only when candle/light installed; not full product MSI |
| Windows **MSI** full product (NSIS parity) | **No** | **No** |
| Windows Authenticode pipeline | **No** (in-repo) | **No** |
| macOS menu bar LaunchAgent | Yes | Yes (dev/ops) |
@ -162,7 +162,7 @@ NSIS product already covers much of the *install experience* (files under Progra
- **[PACKAGING.md](PACKAGING.md)** — tray, menubar, unsigned `.app`, notarization steps (docs), CERT-01, statuslang, IPP split, smoke tests
- [`platform/windows/`](../platform/windows/) — NSIS + tray scripts
- [`platform/windows/wix/`](../platform/windows/wix/) — per-user tray shortcut MSI **skeleton** (candle/light)
- [`platform/windows/wix/`](../platform/windows/wix/) + [`build-tray-msi.ps1`](../platform/windows/scripts/build-tray-msi.ps1) — per-user tray MSI skeleton; ProductVersion ← HelperVersion
- [`platform/macos/pkg/`](../platform/macos/pkg/) — commercial `.pkg` build
- [`platform/macos/menubar/`](../platform/macos/menubar/) — menu bar + unsigned `.app` + notarize docs/checklist
- [`platform/macos/menubar/NOTARIZE-CHECKLIST.md`](../platform/macos/menubar/NOTARIZE-CHECKLIST.md) — menubar notarytool step list

View file

@ -188,7 +188,8 @@ Site page: [`site/printit.verae.ai/certificate.html`](../site/printit.verae.ai/c
| Load paths | Query string (`?sha256=&receipt=&…`), optional `.verae.json` sidecar drop, “Sample data” button |
| Print CSS | Letter page, toolbar hidden, frame colors preserved |
| Live verify | Link to `explorer.verae.ai/time?receipt=&sha256=` (same as `pdfcert.LookupURL`) |
| Residual gap | Full **CERT-01.html** pixel match (self-hosted design-kit Fraunces/Cantarell, live QR bitmap, Substrate extrinsic polish) still **not** claimed — web title ships CDN Fraunces + Georgia/serif offline fallback; issued artifact remains PDF (`internal/pdfcert` + `platform/registration-pdf/build.mjs`) |
| Live QR (web) | **Shipped** — final + receipt/digest paints QR via qrcode.js CDN → data-URL; offline/provisional keeps SVG placeholder (print-safe) |
| Residual gap | Full **CERT-01.html** pixel match (self-hosted design-kit Fraunces/Cantarell, Substrate extrinsic polish) still **not** claimed — web title ships CDN Fraunces + Georgia/serif offline fallback; issued artifact remains PDF (`internal/pdfcert` + `platform/registration-pdf/build.mjs`) |
Design source of truth for the paper mock: `verae-2026-design/ui-designs/src/html/CERT-01.html`.
Cross-product siblings (not owned here): Sign `certificate.html` (SG-P1), Verify ZP-V5, MCP Mac export PDF.

View file

@ -0,0 +1,179 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Build the per-user Verae Tray presence MSI (WiX v3 candle/light) when available.
.DESCRIPTION
Stages verae-tray.ps1 + common.ps1, stamps ProductVersion from
internal/job.HelperVersion via Get-VeraePackageVersion (or $env:VERSION),
then runs candle.exe + light.exe if on PATH (or under Program Files WiX).
If WiX Toolset is missing, prints an install tip and exits 0 without failing
the day-to-day tray path (use build-tray-startmenu.ps1 instead).
Scope: non-elevated per-user tray shortcuts only — not the full commercial MSI.
.PARAMETER Version
Override package version (default: Get-VeraePackageVersion / HelperVersion).
.PARAMETER OutDir
Output directory for .wixobj and .msi (default: platform/windows/wix/out).
.PARAMETER StageDir
Staging directory for payload files (default: platform/windows/wix/Stage).
.PARAMETER WixDir
Directory containing VeraeTrayShortcuts.wxs (+ fragment).
.PARAMETER SkipTip
When candle/light are missing, exit 1 instead of printing the install tip.
.EXAMPLE
powershell -NoProfile -ExecutionPolicy Bypass -File build-tray-msi.ps1
$env:VERSION = "0.3.7"; .\build-tray-msi.ps1
#>
param(
[string]$Version = "",
[string]$OutDir = "",
[string]$StageDir = "",
[string]$WixDir = "",
[switch]$SkipTip
)
$ErrorActionPreference = "Stop"
$common = Join-Path $PSScriptRoot "common.ps1"
if (Test-Path -LiteralPath $common) { . $common }
if (-not $Version) {
if (Get-Command Get-VeraePackageVersion -ErrorAction SilentlyContinue) {
$Version = Get-VeraePackageVersion
} elseif ($env:VERSION -and $env:VERSION.Trim()) {
$Version = $env:VERSION.Trim()
} else {
$Version = "0.3.6"
}
}
if (-not $WixDir) {
$WixDir = Join-Path (Split-Path -Parent $PSScriptRoot) "wix"
}
if (-not $StageDir) {
$StageDir = Join-Path $WixDir "Stage"
}
if (-not $OutDir) {
$OutDir = Join-Path $WixDir "out"
}
$productWxs = Join-Path $WixDir "VeraeTrayShortcuts.wxs"
$fragmentWxs = Join-Path $WixDir "VeraeTrayShortcutsFragment.wxs"
if (-not (Test-Path -LiteralPath $productWxs)) {
Write-Error "Missing WiX product: $productWxs"
}
if (-not (Test-Path -LiteralPath $fragmentWxs)) {
Write-Error "Missing WiX fragment: $fragmentWxs"
}
function Find-WixTool([string]$Name) {
$cmd = Get-Command $Name -ErrorAction SilentlyContinue
if ($cmd -and $cmd.Source) { return $cmd.Source }
$cmdExe = Get-Command "$Name.exe" -ErrorAction SilentlyContinue
if ($cmdExe -and $cmdExe.Source) { return $cmdExe.Source }
$roots = @()
if (${env:ProgramFiles(x86)}) { $roots += ${env:ProgramFiles(x86)} }
if ($env:ProgramFiles) { $roots += $env:ProgramFiles }
if ($env:WIX) {
$wixBin = Join-Path $env:WIX "bin"
if (Test-Path -LiteralPath $wixBin) { $roots = @($wixBin) + $roots }
}
foreach ($root in $roots) {
if (-not $root -or -not (Test-Path -LiteralPath $root)) { continue }
$direct = Join-Path $root "$Name.exe"
if (Test-Path -LiteralPath $direct) { return (Resolve-Path -LiteralPath $direct).Path }
Get-ChildItem -LiteralPath $root -Directory -Filter "WiX Toolset*" -ErrorAction SilentlyContinue |
ForEach-Object {
$bin = Join-Path $_.FullName "bin\$Name.exe"
if (Test-Path -LiteralPath $bin) { return (Resolve-Path -LiteralPath $bin).Path }
}
}
return $null
}
$candle = Find-WixTool "candle"
$light = Find-WixTool "light"
Write-Host "Verae tray presence MSI (skeleton)"
Write-Host " ProductVersion: $Version (HelperVersion / Get-VeraePackageVersion)"
Write-Host " WixDir: $WixDir"
Write-Host " StageDir: $StageDir"
Write-Host " OutDir: $OutDir"
if (-not $candle -or -not $light) {
Write-Host ""
Write-Host "WiX Toolset (candle/light) not found on PATH or under Program Files."
Write-Host "No MSI produced. Day-to-day tray presence:"
Write-Host " powershell -NoProfile -ExecutionPolicy Bypass -File `"$(Join-Path $PSScriptRoot 'build-tray-startmenu.ps1')`""
Write-Host ""
Write-Host "Install WiX tip (Windows host or CI runner):"
Write-Host " 1. Download WiX Toolset v3.14+ from https://wixtoolset.org/ (or winget install --id WiXToolset.WiX)"
Write-Host " 2. Add candle.exe and light.exe to PATH, e.g.:"
Write-Host " set PATH=%PATH%;%ProgramFiles(x86)%\WiX Toolset v3.14\bin"
Write-Host " 3. Re-run this script: build-tray-msi.ps1"
Write-Host " WiX v4 (dotnet tool install --global wix) needs authoring adapted to v4 namespaces."
Write-Host " See platform/windows/wix/README.md"
if ($SkipTip) { exit 1 }
exit 0
}
Write-Host " candle: $candle"
Write-Host " light: $light"
# Stage payload next to .wxs expectations (Stage\verae-tray.ps1, Stage\common.ps1).
New-Item -ItemType Directory -Force -Path $StageDir | Out-Null
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
$traySrc = Join-Path $PSScriptRoot "verae-tray.ps1"
$commonSrc = Join-Path $PSScriptRoot "common.ps1"
if (-not (Test-Path -LiteralPath $traySrc)) {
Write-Error "Missing tray script: $traySrc"
}
Copy-Item -LiteralPath $traySrc -Destination (Join-Path $StageDir "verae-tray.ps1") -Force
if (Test-Path -LiteralPath $commonSrc) {
Copy-Item -LiteralPath $commonSrc -Destination (Join-Path $StageDir "common.ps1") -Force
} else {
Write-Warning "common.ps1 not found; MSI will still require it at light time if Component is present."
}
$stageAbs = (Resolve-Path -LiteralPath $StageDir).Path
$outAbs = (Resolve-Path -LiteralPath $OutDir).Path
$wixAbs = (Resolve-Path -LiteralPath $WixDir).Path
Write-Host "Staging done. Running candle..."
& $candle -nologo -out (Join-Path $outAbs "\") `
"-dProductVersion=$Version" `
"-dStageDir=$stageAbs" `
(Join-Path $wixAbs "VeraeTrayShortcuts.wxs") `
(Join-Path $wixAbs "VeraeTrayShortcutsFragment.wxs")
if ($LASTEXITCODE -ne 0) {
Write-Error "candle failed with exit $LASTEXITCODE"
}
$msiName = "VeraeTray-$Version-peruser.msi"
$msiPath = Join-Path $outAbs $msiName
Write-Host "Running light -> $msiPath"
& $light -nologo -out $msiPath `
(Join-Path $outAbs "VeraeTrayShortcuts.wixobj") `
(Join-Path $outAbs "VeraeTrayShortcutsFragment.wixobj")
if ($LASTEXITCODE -ne 0) {
Write-Error "light failed with exit $LASTEXITCODE"
}
Write-Host ""
Write-Host "Built: $msiPath"
Write-Host " ProductVersion=$Version (from HelperVersion / Get-VeraePackageVersion)."
Write-Host " Per-user, non-elevated tray presence only — not full commercial MSI."
Write-Host " Silent: msiexec /i `"$msiPath`" /qn VERAE_IPP_ADDR=127.0.0.1:8631"
Write-Host " Hold ids are never evidence."
exit 0

View file

@ -53,7 +53,25 @@ copy /Y platform\windows\scripts\common.ps1 %STAGE%\
Elevated Program Files install, printers, scheduled helper, Authenticode,
NSIS key wizard parity, machine-wide Start Menu under ProgramData.
### Build (WiX v3 candle / light)
### Build (preferred): `build-tray-msi.ps1`
From `platform/windows/scripts` on Windows (or any cwd):
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File platform\windows\scripts\build-tray-msi.ps1
```
The script:
1. Resolves **ProductVersion** via `Get-VeraePackageVersion` → `internal/job.HelperVersion`
(override with `-Version` or `$env:VERSION`).
2. Stages `verae-tray.ps1` + `common.ps1` under `wix/Stage`.
3. Runs **candle** + **light** when found on PATH / `%WIX%` / Program Files.
4. Writes `wix/out/VeraeTray-<version>-peruser.msi`.
5. If candle/light are missing, prints a **WiX install tip** and exits 0
(day-to-day tray path stays `build-tray-startmenu.ps1`).
### Build (manual WiX v3 candle / light)
From repo root on Windows:
@ -61,6 +79,7 @@ From repo root on Windows:
set WIXDIR=platform\windows\wix
set STAGE=%WIXDIR%\Stage
set OUT=%WIXDIR%\out
REM Prefer HelperVersion; build-tray-msi.ps1 does this automatically:
set VER=0.3.6
mkdir %STAGE% 2>nul
@ -81,9 +100,9 @@ light.exe -nologo -out %OUT%\VeraeTray-%VER%-peruser.msi ^
Notes:
- Set `VER` from `internal/job.HelperVersion` (see `docs/PACKAGING.md`), or
pass `-dProductVersion=` explicitly. The `.wxs` defaults to `0.3.6` when
unset (HelperVersion at scaffold time).
- **ProductVersion** is wired by `build-tray-msi.ps1` → `Get-VeraePackageVersion`
/ `HelperVersion`. Manual builds should pass `-dProductVersion=` the same way.
The `.wxs` `<?ifndef?>` fallback is `0.3.6` (must match current HelperVersion).
- `Product Id="*"` generates a new ProductCode per build; `UpgradeCode` is
fixed for major-upgrade of this **tray presence** package only.
- Skeleton is **UI-less** (silent-friendly). For WixUI_Minimal, uncomment
@ -102,9 +121,10 @@ Uninstall via ARP (**Verae Tray (presence)**) or `msiexec /x {ProductCode} /qn`.
| File | Role |
|------|------|
| `VeraeTrayShortcuts.wxs` | Per-user Product + file components |
| `VeraeTrayShortcuts.wxs` | Per-user Product + file components (`ProductVersion`) |
| `VeraeTrayShortcutsFragment.wxs` | Start Menu + Startup shortcut fragment |
| `README.md` | This file |
| `../scripts/build-tray-msi.ps1` | candle/light driver; wires ProductVersion; WiX tip if missing |
### Relation to other Windows paths
@ -113,6 +133,7 @@ Uninstall via ARP (**Verae Tray (presence)**) or `msiexec /x {ProductCode} /qn`.
| `../scripts/verae-tray.ps1` | Runtime tray (source staged into MSI) |
| `../scripts/install-tray-startup.ps1` | WiX-free Startup shortcut |
| `../scripts/build-tray-startmenu.ps1` | WiX-free Start Menu + Startup |
| `../scripts/build-tray-msi.ps1` | Builds this skeleton MSI when WiX is installed |
| `../installer-windows.nsi` | Elevated **full product** setup.exe |
| **`wix/` (this folder)** | Per-user tray shortcut **MSI skeleton** |

View file

@ -11,8 +11,9 @@
Requires WiX Toolset (candle + light, or WiX v4 `wix build`) to produce MSI.
See README.md in this folder.
Version default tracks internal/job.HelperVersion; override at candle time:
-dProductVersion=x.y.z
ProductVersion: prefer -dProductVersion from build-tray-msi.ps1
(Get-VeraePackageVersion → internal/job.HelperVersion, or VERSION=).
Fallback below must match HelperVersion when candle is invoked bare.
-->
<?ifndef ProductVersion ?>
<?define ProductVersion = "0.3.6" ?>

View file

@ -69,10 +69,14 @@
}
.qr{
width:112px;height:112px;flex:none;border:2px solid var(--ink,#1D1534);border-radius:8px;
/* Offline / provisional placeholder — print-safe without CDN */
background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 29 29'%3E%3Cpath fill='%231D1534' d='M1 1h7v7H1zm2 2v3h3V3zm10 0h7v7h-7zm2 2v3h3V3zM1 13h7v7H1zm2 2v3h3v-3zm18-14h7v7h-7zm2 2v3h3V3zm0 10h3v2h-3zm-4 0h2v2h-2zm4 4h3v3h-3zm-4 2h2v3h-2zm-2-4h2v2h-2zm6 6h3v2h-3zm-8 0h2v2h-2zm4-2h2v4h-2zm-6 0h2v2h-2z'/%3E%3C/svg%3E") center/72% no-repeat;
position:relative;
position:relative;overflow:hidden;
}
.qr span{position:absolute;inset:auto;left:50%;top:50%;transform:translate(-50%,-50%);background:#fff;padding:2px 5px;font:700 10px system-ui;color:var(--plum,#531B59)}
.qr .qr-fallback{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);background:#fff;padding:2px 5px;font:700 10px system-ui;color:var(--plum,#531B59);pointer-events:none}
.qr img.qr-bitmap,.qr canvas.qr-bitmap{width:100%;height:100%;display:block;border-radius:6px;object-fit:contain;background:#fff}
.qr.has-qr{background:#fff}
.qr.has-qr .qr-fallback{display:none}
.verify-box ol{margin:6px 0 0 16px;padding:0;line-height:1.6;color:var(--ink-soft,#3B3250);font-size:12.5px}
.verify-box a{color:var(--info,#3B2A8C);font-size:10px;word-break:break-all}
.foot{
@ -97,8 +101,11 @@
}
/* CDN may be absent offline; keep plum/gold frame + system serif title */
.title{font-family:'Fraunces',Georgia,'Times New Roman',Times,serif!important}
.rule,.banner,.mh,.qr{ -webkit-print-color-adjust:exact;print-color-adjust:exact }
.rule,.banner,.mh,.qr,.qr img.qr-bitmap,.qr canvas.qr-bitmap{
-webkit-print-color-adjust:exact;print-color-adjust:exact
}
.rule{background:#C4A85A!important}
.qr{width:96px;height:96px;border-color:#1D1534!important}
a{color:inherit;text-decoration:none}
}
@page{size:letter;margin:12mm}
@ -164,11 +171,13 @@
</dl>
<div class="verify-box">
<div class="qr" aria-hidden="true"><span>Verae</span></div>
<div class="qr" id="qr" role="img" aria-label="Verify QR — explorer lookup when final">
<span class="qr-fallback">Verae</span>
</div>
<div>
<b style="font-size:12.5px">Verify this certificate</b>
<ol>
<li>Open the explorer link (or scan the QR on a printed PDF).</li>
<li>Open the explorer link (or scan the QR when final).</li>
<li>Compare the local file’s SHA-256 to the multihash below — file never uploaded.</li>
<li>Only a <b>final</b> / already-registered receipt is evidence. Hold ids are not proof.</li>
</ol>
@ -192,7 +201,8 @@
<span class="mono">?document=&amp;sha256=&amp;receipt=&amp;created_at=&amp;merkle=&amp;blake3=&amp;block=&amp;helper_version=</span>
Multihash / BLAKE3 lines appear <b>only when data is present</b> — never invented.
Title uses Fraunces (Google Fonts CDN) with Georgia/serif offline fallback; plum/gold frame kept.
Full CERT-01.html pixel match (self-hosted design-kit fonts, live QR) remains a residual.
Final certificates paint a live QR (qrcode.js CDN → data-URL) from the explorer lookup; offline / provisional keeps the SVG placeholder so print still works.
Residual: full CERT-01.html self-hosted Fraunces/Cantarell pixel match (design-kit fonts).
</p>
</div>
</div>
@ -283,6 +293,70 @@
return u;
}
/** Live QR when final + payload; CDN fail / offline / provisional → SVG placeholder (print-safe). */
function clearQR(el) {
if (!el) return;
el.classList.remove('has-qr');
var bit = el.querySelectorAll('img.qr-bitmap, canvas.qr-bitmap');
for (var i = 0; i < bit.length; i++) bit[i].remove();
}
function paintQRDataUrl(el, url) {
if (!el || !url) return;
clearQR(el);
var img = document.createElement('img');
img.className = 'qr-bitmap';
img.alt = '';
img.width = 112;
img.height = 112;
img.decoding = 'async';
img.onload = function () { el.classList.add('has-qr'); };
img.onerror = function () { img.remove(); /* keep placeholder */ };
img.src = url;
el.appendChild(img);
}
function withQRCodeLib(fn) {
if (window.QRCode && typeof window.QRCode.toDataURL === 'function') {
fn(window.QRCode);
return;
}
var existing = document.querySelector('script[data-verae-qrcode-lib]');
if (existing) {
existing.addEventListener('load', function () {
if (window.QRCode && window.QRCode.toDataURL) fn(window.QRCode);
});
return;
}
var s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/qrcode@1.5.4/build/qrcode.min.js';
s.async = true;
s.setAttribute('data-verae-qrcode-lib', '1');
s.onload = function () {
if (window.QRCode && window.QRCode.toDataURL) fn(window.QRCode);
};
/* onerror: leave placeholder — offline print fallback */
document.head.appendChild(s);
}
function setVerifyQR(payload, isFinal) {
var el = $('qr');
if (!el) return;
clearQR(el);
if (!isFinal || !payload) return;
withQRCodeLib(function (QRCode) {
QRCode.toDataURL(String(payload), {
errorCorrectionLevel: 'M',
margin: 1,
width: 224,
color: { dark: '#1D1534', light: '#FFFFFF' }
}, function (err, dataUrl) {
if (err || !dataUrl) return;
paintQRDataUrl(el, dataUrl);
});
});
}
function apply(data) {
data = data || {};
var doc = data.document_name || data.document || data.file || '';
@ -351,6 +425,10 @@
var a = $('verify-link');
a.href = url;
a.textContent = url;
/* Real QR only when final evidence + receipt or digest; else placeholder. */
var qrPayload = (sha || receipt || mh) ? url : '';
setVerifyQR(qrPayload, evidence && !!qrPayload);
}
function fromQuery() {