Gitea is master and GitHub is a mirror, but the Windows and Linux jobs still
only existed in .github/workflows, so GitHub was doing real build work. This
moves Windows.
Two jobs on two machines, which is the point rather than an accident:
build runs on `deb` (windows-latest:host — it carries the MinGW
cross-compiler) and produces an unsigned wowlet.exe.
sign runs on desktop-iim2fs2 (windows:host), the machine with the EV
code-signing token physically in it.
The signing job does not itself sign. Its runner is NT SERVICE\act_runner,
which cannot use the token: SafeNet holds the login per user and the login is
established interactively by an operator. So the job queues the binary for the
Fleet-installed broker (roles/windows_code_signing) and waits. CI never holds
the key and never holds the token password, so it cannot spend one of the
token's three password attempts.
Differences from the GitHub lane, all because this is a real fleet host rather
than a disposable hosted runner:
- No apt install and no sudo. Every dependency is already present on deb, and a
first step asserts the toolchain — including that mingw is the posix
threading variant — and fails pointing at such-fleet rather than half
building.
- No disk-freeing or fallocated swap. Those existed to survive a hosted
runner's 16G/14G budget; deb has 30G and 3.1T free.
- depends builds at -j4 and wowlet at -j6 rather than -j2 for the same reason.
The exe is staged unzipped between the jobs because the broker signs one PE at
a time; the published zip is assembled after signing, so the archive contains
the signed binary. The signature, its RFC 3161 timestamp and the expected
signer are all re-verified on the artifact that actually ships.
Dispatch-only: both runners are capacity 1 and shared with other lanes.
Every tab was running the identical eight seconds forever. Three changes:
- Each lap rolls its own timing: 3-5s of empty field, and the money now enters
on its own delay at its own pace instead of shadowing the doge every time.
- Roughly one lap in six the doge eats it partway down the field: he tips
nose-first onto the grass, GAME OVER comes up, and the coins he never reached
are still sitting there. Costs no new art — it is the running sprite pivoted
on its own feet.
- A `variant` on SceneWidget picks the sky: dusk (History, lock screen), dawn
(Receive), night (Send, with stars and a moon instead of the sun).
The cycle is now a timer-driven state machine rather than one SequentialAnimation.
That is not stylistic: a lap that rolls its own durations has to assign them
somewhere, and assigning a duration to a child of a *running* SequentialAnimation
makes the group recompute and re-enter its own ScriptAction until the JS stack
blows. Qt flags the binding form of the same mistake as a binding loop.
Verified by headless qml6 render: a forced trip lap, and a clean run with no QML
warnings.
The build stages the wownerod binary into src/assets/wownerod/ and generates
src/assets_wownerod.qrc, exactly as it does for tor and docs — but only the tor
and docs paths were ignored. A `git add -A` in a build tree therefore offered a
24MB binary and a generated qrc for commit. Ignore them the same way.
Send had a large empty region under the form doing nothing, and Receive had
nothing at all. Both get the scene back — but as real layout items, not as the
overlay that painted over the forms last time.
Send: the form only ever needed the top of the tab, so the QFormLayout is now
wrapped in a vbox and the scene takes the rest. The widget's vertical size
policy was Maximum, which made it hug the form; it owns the whole tab now. The
form itself is untouched, and nothing is drawn on top of the address or amount
fields.
Receive: a short banner under the address table.
Built and smoke-tested from the AppImage lane.
The lap ran flat out and reset the moment the doge left the frame. It now
breathes:
1. ~4s of empty field, nothing but sky and grass
2. the coins pop in one at a time (320ms apart)
3. the doge and the money cross, and the doge collects them
Both coin states stay derived from the cycle rather than tracked: a coin is out
once `coinReveal` passes its index, and collected once the doge's snout reaches
its centre. Dropping `coinReveal` to 0 and `runProgress` to 0 at the top of the
loop therefore restores the whole field with no reset bookkeeping, and parks
the doge and the money off the left edge for the empty beat at the same time.
The money now crosses with the doge instead of drifting on its own clock, a
little quicker so it leads him off screen, and it sits proportionally in the
sky so a tall scene does not pin it to the top edge.
Verified by headless qml6 render across a full cycle at 1000x200.
The metrics kept sliding out of the bottom of the node console. The cause was
free height, not margins: the top bar's RowLayout, its right-hand ColumnLayout
and the console RowLayout all defaulted to fillHeight, so any slack in the
window was absorbed by the console. The 102px bezel graphic stretched with it
(topright_middle is a 1px column tiled horizontally), the label/value rows —
two fillHeight halves — spread apart with it, and a full block height ended up
rendered below the panel. The three `Layout.preferredHeight: parent.height`
bindings, which asked a layout child for its own parent layout's height, made
the result depend on resolution order.
Now the top bar, its column and the console are pinned to their design heights
(128 / 128 / 102), the bezel images take the literal 102, and the metrics are a
plain anchored Row of two Columns at a fixed offset inside the console's dark
interior band (rows 7..94 of the bezel) with the value directly under its
label.
Verified by headless qml6 render at 1029x{500,597,650,720,800} with a full-width
block height ("369918/369918"): the pair sits in the same place at every height
and stays inside the panel.
IdleDoge scaled its movie from `currentImage()` after `jumpToFrame(0)`, but the
first frame is not decoded at that point, so the size came back null, the
scaling was skipped, and the QLabel ended up at the GIF's full 500x397. Parked
at the bottom-right of the form it painted an opaque 500x397 slab of window
background over the lower half of Send and over the Receive address table.
Send is the screen where money leaves the wallet; it does not get an overlay
sitting on top of the address and amount fields at all. Receive loses it for
the same reason. SendWidget.cpp and ReceiveWidget.cpp are back to exactly their
pre-ambient state, and the widget is removed rather than left unused.
The ambient scene keeps the places where it has its own real estate: the lock
screen, the History banner, and the mining tab.
Three things you spotted on the History banner:
- Slow first lap that restarted at speed. The dog and the money animated
`to: scene.width`, and the QQuickWidget starts at a placeholder width: the
first lap crawled across the wrong distance, then the real width arrived,
re-bound `to`, and restarted the animation. Motion is now driven by a
unitless 0..1 progress and mapped onto the current width each frame, so a
resize repositions without restarting.
- Coins sat mid-sky and clipped the top border. They now rest on the grass
with a low bob, at the doge's pickup height.
- The doge collects them. A coin is collected once the doge's snout reaches
its centre: it pops, fades, and floats a "+1". The collected state is
derived from the run position, so the coins simply come back when the lap
restarts, with nothing to reset.
Reduced motion still gets a static field: doge parked at the left, every coin
present, money parked in the sky.
Verified by headless qml6 render at banner (1000x140, 620x120) and lock-screen
(900x380) sizes, motion and reduced-motion, with no QML warnings.
- Lock screen: full ambient scene above the unlock form (the idle 'screensaver'
— wallet auto-locks here) instead of a thin banner.
- History ('home'): full scene as a top banner.
- Send/Receive: IdleDoge — a small click-through sitting doge in the corner,
the 'most quiet' idle for task screens.
- Mining keeps its own scene.
All gated by Config::reducedMotion (scenes freeze, doge shows a still frame).
Reusable SceneWidget (QQuickWidget host) loads scene.qml — dusk sky, tiled
grass, running doge, bouncing WOW coins (slowed per feedback), drifting
money — and feeds it Config::reducedMotion so it freezes to a still when
that's set. First placement: a banner atop the unlock form. Scene rendered
& verified at banner + screensaver aspects via local qml6 harness. Assets
from wow-lite-wallet (friend's). Home banner / idle screensaver / per-tab
idle to follow on the same SceneWidget.
Config::reducedMotion (default false = playful animations ON). Surfaced as a
checkbox on first run (wizard PageMenu) and in Settings → Appearance —
unchecked by default, matching the OS 'Reduce Motion' convention. The mining
tab's AnimatedImages now freeze when it's set (MiningBackend exposes it to
mining.qml). Foundation for gating the ambient silliness to come.
The value sat ~30px below its label (label centered in top cell, value
centered in bottom cell), so long values like the block height floated low
in the console. Anchor labels to the bottom of their cell and values to the
top of theirs so each metric reads as a tight pair. Verified via local
qml6 render.
The wallet-open splash now shows a random loader from a bundled set (13
curated GIFs from wownero/wow-lite-wallet, ~2.2MB) instead of the static
key icon — auto-discovered from :/silly, aspect-scaled to 56px, falls back
to the key pixmap if none present. Assets by a friend (same Wownero family).
- macOS: first native signed + notarized build (new platform → minor bump)
- fix: stuck-tx detection + rebroadcast + re-relay on daemon sync
(resolves the 'unconfirmed for days' field report)
- mining tab: top text layout fixes
macdeployqt bundles Qt frameworks but not the plain @rpath/*.dylib brew
deps of its image plugins, so a no-Homebrew Mac couldn't load webp. Add a
Qt-safe pass that resolves @rpath/*.dylib (never @rpath/*.framework) from
brew, copies them in, rewrites load paths, and fails the build if any
remain. Sign step now re-signs by Mach-O type (not exec bit) so the
install_name_tool-modified plugins get valid signatures.
The 100%-linked app dynamically referenced brew's libboost_*.dylib, which
macdeployqt failed to bundle — the app would fail to launch on any Mac
without Homebrew (notarization doesn't check runtime dylib resolution).
Static boost matches how guix/depends release builds work and removes the
runtime dependency entirely.
monero's static libs built fine, but the feather target includes monero
headers pulling boost/variant.hpp, and clang doesn't search
/opt/homebrew/include by default (modern BoostConfig leaves
Boost_INCLUDE_DIRS empty).
brew's protobuf pulls Abseil headers that require C++17, breaking the
feather compile (DEVICE_TREZOR_READY gates the protobuf link). Trezor
hardware does not support Wownero, so this removes a useless dependency
rather than working around it — the Linux lane only tolerates it because
its depends system pins an old protobuf.
- Sync Progress value had both left and horizontalCenter anchors (drifted center)
- 20pt labels in 30px rows collided; now 14pt matching the top-left monitors
- hashrate/uptime/height values auto-fit their pixel-art frames (HorizontalFit)
- 'uptime' -> 'Uptime'
Dispatch-only workflow in .gitea/workflows (GitHub ignores the
directory): brew dependencies, native cmake build with the embedded
wownerod fetched per-arch, unsigned app bundle artifact. Runs on the
mac runner whose work dir lives on DataSSD and is outside every backup
path. Signing/notarization follows once a Developer ID Application
certificate is provisioned as repo secrets.
Field report: a send sat 'in mempool' for two days while the network had
never seen it (Tor down at send time defeats both the multi-broadcast and
the embedded daemon's exclusive --tx-proxy relay), locking the change.
Three additive changes, all strictly local (no txid is ever queried
against an external service):
- onMultiBroadcast now counts relay responses; if no node accepted the
broadcast within 45s the user is told immediately and offered the
existing rebroadcast path, instead of silence.
- A periodic local history scan warns once per outgoing tx that has been
pending for more than 25 minutes, again offering rebroadcast (which
already reconnects to a fresh node first).
- On DaemonManager::synced, still-pending outgoing txs are re-announced
from the wallet's own tx cache; a restarted embedded node's pool has
forgotten them.
On top of Lucas's #119 (--db-sync-mode safe):
- DaemonManager: age out the crash-retry budget (m_restarts) so legit node
toggles/restarts don't trip "maximum retries exceeded" after ~4 spawns over a
session; the cap now only bites a genuine rapid crash-loop.
- DaemonManager: clear the m_stopping latch on start(), so the crash auto-restart
keeps working after the node has been toggled off then on (it was a one-shot
before).
- bump version 5.0.2 -> 5.0.3.
- wizard: hide the hardware-device wallet option (wownero has no Ledger/Trezor
support; inherited Feather cruft).
- bump version 5.0.1 -> 5.0.2.
v5.0.2 also rebuilds the Linux AppImage in an ubuntu:24.04 container (glibc 2.39)
so it runs on Ubuntu 24.04 / Mint / Debian — v5.0.1's AppImage was built on
glibc 2.43 and would not start elsewhere. No wallet-code change: wallets created
on pre-table-fix builds carry a stale restore height (blank balance); the fix is
to re-restore from seed.
restore_heights_wownero_mainnet.txt was last updated 2023-12-15 (h587048).
For wallets created after that, RestoreHeightLookup::dateToHeight extrapolates
forward at 288 blk/day; 2.5y past the last anchor overshoots the chain tip, so
the computed restore height lands ABOVE the current height. The wallet then
skips output-scan on every real block => received funds are never recorded,
while the tx pool still shows them: coins appear pending, then vanish.
Same bug class as 8686e9bf (which fixed the block-time CONSTANT from monero's
120s/720-per-day to wownero's 300s/288); here the constant was right but the
TABLE went stale. Regenerated with contrib/generate-restore-heights/heights.py
to h849000 (2026-06-19). Verified dateToHeight < tip for all dates 2019..today.
Windows crash chain (exposed by the embedded-node default):
- main.cpp: force the QtQuick software renderer. The Mining tab's QQuickWidget
drove the default RHI/GL path and faulted (0xc0000005) on broken-GL hosts
(VirtualBox VMSVGA), crashing the app and orphaning the embedded node.
Real hardware was unaffected.
- ChildProcessGuard + DaemonManager/TorManager: kernel-level die-with-parent
(Windows Job Object KILL_ON_JOB_CLOSE / Linux PR_SET_PDEATHSIG) so the
embedded wownerod and bundled tor never survive a wowlet exit or crash.
- DaemonManager: verify-before-adopt via get_info (nettype/height) instead of
trusting a bare open RPC port, so a stale/orphan node is never adopted.
Node/Tor lifecycle + remote-node connectivity:
- config: torPrivacyLevel default -> allTorExceptNode. NEVER sync blocks over
Tor; the wallet's node connection is always clearnet. Tor is only for tx
broadcast, so remote nodes work with or without Tor.
- nodes: add Nodes::reconnect() (clear failure blocklist + force reconnect);
node/Tor toggle handlers use it so a transient outage no longer strands the
wallet at "Disconnected".
- MainWindow/SettingsDialog: reconnect on node/Tor toggle and on the async
daemonStateChanged (closes the start() race; a freshly-toggled local node
connects immediately).
- TorManager: reset retry/unpack state in init() so a system<->bundled Tor
re-toggle can start again; quiesce on stop(). Linux unpackBins() rework
(mkpath + presence-based unpack + graceful return-false), getVersion() arg
fix, and a missing-binary start guard.
- build.yml: derive TOR_VERSION from the embedded tor binary (a hardcoded
bundle number made unpackBins rewrite Tor every launch).
- depends/funcs.mk: confine cross-cmake find_package to the depends sysroot so
host -dev libraries don't leak into the build.
v5.0.0 ships Windows x86_64 + Linux x86_64. arm64/riscv64 (Qt static-link gap) and macOS deferred to v5.0.1. continue-on-error keeps the cross job from failing the release run while I keep iterating it.
libxkbcommon 1.13.1 meson.build adds empty pkgconfig vars when xkeyboard-config is absent from the cross sysroot (XKEYBOARD_CONFIG_ROOT='') while has_extensions_directories is true -> meson hard-errors at pkgconfig.generate. Pass non-empty -Dxkb-config-(un)versioned-extensions-path.
Add restore-keys to all 4 depends cache restores: a busted exact key (any packages/* edit) now falls back to the newest prefix match, so make resumes from the partial built tree instead of rebuilding the static-Qt6 toolchain from scratch.
The find debug showed wayland-scanner.pc at <build_prefix>/lib/x86_64-linux-gnu/pkgconfig (multiarch layout), not lib/pkgconfig or share/pkgconfig. Add that path to the pkg-config vars so cross-meson finds the native scanner. Keeps native Wayland support (no disabling).
wayland-scanner.pc is a noarch .pc -> build_prefix/share/pkgconfig, not lib/pkgconfig, so the earlier PKG_CONFIG_PATH_FOR_BUILD missed it. Add share/pkgconfig to LIBDIR/PATH/PATH_FOR_BUILD + a find to log the real location.
Target wayland (-Dscanner=false) resolves the host scanner via dependency(..., native: true), which uses meson's BUILD-machine pkg-config — not pointed at build_prefix. Add PKG_CONFIG_PATH_FOR_BUILD=build_prefix + a [binaries] wayland-scanner in the cross-file. Fixes 'Dependency wayland-scanner not found' on aarch64/riscv64.
Cross-build wowlet via depends for aarch64/riscv64 (reusing the zeromq/ICU/qt/testlib fixes), embed the matching wownerod + depends-built tor, package an AppImage via contrib/AppImage/build-appimage.sh (its hardcoded /feather runtime path relativized to GITHUB_WORKSPACE). Cached per-host, save-always.