Field report: "the View Logs button now appears on the client, but when I click it, it opens my Documents folder instead of the correct logs directory."
Nothing is wrong with the button — the path is.
Root cause
The client ships as a full-trust MSIX package (runFullTrust), and Windows redirects a packaged app's %LOCALAPPDATA% writes into its private …\Packages\<family>\LocalCache\Local\. logfile creates and appends through that redirection without ever seeing it, so the literal %LOCALAPPDATA%\punktfunk\logs it hands out is right to write to yet names a directory that never exists on disk.
Explorer runs outside the container: it resolves the literal path, finds nothing, and — instead of failing — silently falls back to the user's Documents folder. That silent fallback is the entire signature.
An unpackaged dev run creates that directory for real, which is why this only ever showed up in the field.
It was three bugs, not one
The same phantom path was handed straight to the user in two more places, both added by d839f4c2 and both wrong the same way:
the client log file startup line, and
the failed-spawn banner's Check <path> — the one people are told to follow after a session dies.
Anyone who followed either landed in an empty or absent directory.
The fix
One resolver, not three call-site patches:
real_dir() canonicalizes the directory it just created. That resolves through the redirection on a packaged run and changes nothing on an unpackaged one — no package identity to detect, no LocalCache path to hand-assemble.
log_dir() stays the write path and goes private, so a future caller can't reach for the wrong one.
path() resolves too, which fixes both messages.
strip_verbatim() undoes the \\?\ that canonicalize always returns — Explorer refuses verbatim paths, taking the same silent Documents fallback — including the \\?\UNC\ form a roaming profile on a share resolves to.
The button additionally guards on is_dir().
Not affected: the art-cache, os-icons and %APPDATA% trust store are app-internal, read and written through the same redirection, so they stay self-consistent. The host is an Inno Setup install and its tray uses %ProgramData%, which is not redirected.
Verification
Run on the Windows CI runner (.133), since this file is cfg(windows) and macOS never compiles it:
cargo test -p punktfunk-client-windows — green, incl. 5 new logfile::tests
cargo fmt --all --check — clean
Measured GetFinalPathNameByHandleW on the box directly: it does return \\?\, so the prefix stripping is load-bearing; and on an unpackaged run it resolves to the literal path, proving this change is a no-op there and cannot regress anything.
⏳ Known gap — the packaged half is unverified
That canonicalize resolves through the MSIX redirection on a genuinely packaged run is the one load-bearing assumption, and it is not yet confirmed on glass. Note the is_dir() guard does not catch it if that is wrong: from inside the container the literal path appears to exist, so a bad resolve would quietly revert to today's behaviour rather than doing nothing.
Confirming needs an MSIX install plus a real desktop session (.173/.221 were off-LAN). Two headless workarounds dead-ended and are not worth repeating: Invoke-CommandInDesktopPackage into a stock UWP package can't host a probe (AppContainer), and New-SelfSignedCertificate won't persist a key over ssh on .133, so a hand-rolled signed full-trust test package isn't buildable there either.
Field report: *"the View Logs button now appears on the client, but when I click it, it opens my Documents folder instead of the correct logs directory."*
Nothing is wrong with the button — the path is.
## Root cause
The client ships as a **full-trust MSIX package** (`runFullTrust`), and Windows redirects a packaged app's `%LOCALAPPDATA%` writes into its private `…\Packages\<family>\LocalCache\Local\`. `logfile` creates and appends through that redirection without ever seeing it, so the literal `%LOCALAPPDATA%\punktfunk\logs` it hands out is right to **write** to yet names a directory that **never exists on disk**.
Explorer runs *outside* the container: it resolves the literal path, finds nothing, and — instead of failing — **silently falls back to the user's Documents folder**. That silent fallback is the entire signature.
An unpackaged dev run creates that directory for real, which is why this only ever showed up in the field.
## It was three bugs, not one
The same phantom path was handed straight to the user in two more places, both added by `d839f4c2` and both wrong the same way:
* the `client log file` startup line, and
* the failed-spawn banner's `Check <path>` — the one people are told to follow *after a session dies*.
Anyone who followed either landed in an empty or absent directory.
## The fix
One resolver, not three call-site patches:
* **`real_dir()`** canonicalizes the directory it just created. That resolves *through* the redirection on a packaged run and changes nothing on an unpackaged one — no package identity to detect, no LocalCache path to hand-assemble.
* **`log_dir()`** stays the write path and goes **private**, so a future caller can't reach for the wrong one.
* **`path()`** resolves too, which fixes both messages.
* **`strip_verbatim()`** undoes the `\\?\` that `canonicalize` always returns — Explorer refuses verbatim paths, taking the *same* silent Documents fallback — including the `\\?\UNC\` form a roaming profile on a share resolves to.
* The button additionally guards on `is_dir()`.
Not affected: the art-cache, os-icons and `%APPDATA%` trust store are app-internal, read and written through the same redirection, so they stay self-consistent. The host is an Inno Setup install and its tray uses `%ProgramData%`, which is not redirected.
## Verification
Run on the Windows CI runner (.133), since this file is `cfg(windows)` and macOS never compiles it:
* `cargo clippy -p punktfunk-client-windows --all-targets -- -D warnings` — clean
* `cargo test -p punktfunk-client-windows` — green, incl. 5 new `logfile::tests`
* `cargo fmt --all --check` — clean
* Measured `GetFinalPathNameByHandleW` on the box directly: it does return `\\?\`, so the prefix stripping is load-bearing; and on an unpackaged run it resolves to the literal path, proving this change is a **no-op there** and cannot regress anything.
## ⏳ Known gap — the packaged half is unverified
That `canonicalize` resolves *through* the MSIX redirection on a genuinely packaged run is the one load-bearing assumption, and it is not yet confirmed on glass. Note the `is_dir()` guard does **not** catch it if that is wrong: from inside the container the literal path *appears* to exist, so a bad resolve would quietly revert to today's behaviour rather than doing nothing.
Confirming needs an MSIX install plus a real desktop session (.173/.221 were off-LAN). Two headless workarounds dead-ended and are not worth repeating: `Invoke-CommandInDesktopPackage` into a stock UWP package can't host a probe (AppContainer), and `New-SelfSignedCertificate` won't persist a key over ssh on .133, so a hand-rolled signed full-trust test package isn't buildable there either.
The button shipped in d839f4c2 opens the user's Documents folder instead of the log
directory on every packaged install. Nothing is wrong with the button — the path is.
The client ships as a full-trust MSIX package, and Windows redirects a packaged app's
%LOCALAPPDATA% writes into its private ...\Packages\<family>\LocalCache\Local\. The log
module creates and appends through that redirection without ever seeing it, so the
literal %LOCALAPPDATA%\punktfunk\logs it hands out is right to WRITE to and names a
directory that never exists on disk. Explorer runs outside the container: it resolves the
literal path, finds nothing, and — instead of failing — silently falls back to Documents.
An unpackaged dev run creates that directory for real, which is why this only ever showed
up in the field.
Two more places handed the same phantom path straight to the user, both added by the same
commit and both wrong in the same way: the "client log file" startup line, and the
failed-spawn banner's "Check <path>" — the one people are told to follow after a session
dies. Anyone who did landed in an empty or absent directory.
So the fix is one resolver, not three call-site patches. `real_dir` canonicalizes the
directory it just created, which resolves through the redirection on a packaged run and
changes nothing on an unpackaged one — no package identity to detect, no LocalCache path
to hand-assemble. `log_dir` stays as the write path and goes private so a future caller
can't reach for the wrong one; `path` now resolves too, which fixes both messages.
`canonicalize` always returns a `\\?\` verbatim path and Explorer refuses those (taking
the same silent Documents fallback), so `strip_verbatim` undoes the prefix — including
the `\\?\UNC\` form a roaming profile on a share resolves to. The button additionally
guards on `is_dir()`: if the resolve ever comes back wrong, the click does nothing rather
than landing the user somewhere misleading again.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Field report: "the View Logs button now appears on the client, but when I click it, it opens my Documents folder instead of the correct logs directory."
Nothing is wrong with the button — the path is.
Root cause
The client ships as a full-trust MSIX package (
runFullTrust), and Windows redirects a packaged app's%LOCALAPPDATA%writes into its private…\Packages\<family>\LocalCache\Local\.logfilecreates and appends through that redirection without ever seeing it, so the literal%LOCALAPPDATA%\punktfunk\logsit hands out is right to write to yet names a directory that never exists on disk.Explorer runs outside the container: it resolves the literal path, finds nothing, and — instead of failing — silently falls back to the user's Documents folder. That silent fallback is the entire signature.
An unpackaged dev run creates that directory for real, which is why this only ever showed up in the field.
It was three bugs, not one
The same phantom path was handed straight to the user in two more places, both added by
d839f4c2and both wrong the same way:client log filestartup line, andCheck <path>— the one people are told to follow after a session dies.Anyone who followed either landed in an empty or absent directory.
The fix
One resolver, not three call-site patches:
real_dir()canonicalizes the directory it just created. That resolves through the redirection on a packaged run and changes nothing on an unpackaged one — no package identity to detect, no LocalCache path to hand-assemble.log_dir()stays the write path and goes private, so a future caller can't reach for the wrong one.path()resolves too, which fixes both messages.strip_verbatim()undoes the\\?\thatcanonicalizealways returns — Explorer refuses verbatim paths, taking the same silent Documents fallback — including the\\?\UNC\form a roaming profile on a share resolves to.is_dir().Not affected: the art-cache, os-icons and
%APPDATA%trust store are app-internal, read and written through the same redirection, so they stay self-consistent. The host is an Inno Setup install and its tray uses%ProgramData%, which is not redirected.Verification
Run on the Windows CI runner (.133), since this file is
cfg(windows)and macOS never compiles it:cargo clippy -p punktfunk-client-windows --all-targets -- -D warnings— cleancargo test -p punktfunk-client-windows— green, incl. 5 newlogfile::testscargo fmt --all --check— cleanGetFinalPathNameByHandleWon the box directly: it does return\\?\, so the prefix stripping is load-bearing; and on an unpackaged run it resolves to the literal path, proving this change is a no-op there and cannot regress anything.⏳ Known gap — the packaged half is unverified
That
canonicalizeresolves through the MSIX redirection on a genuinely packaged run is the one load-bearing assumption, and it is not yet confirmed on glass. Note theis_dir()guard does not catch it if that is wrong: from inside the container the literal path appears to exist, so a bad resolve would quietly revert to today's behaviour rather than doing nothing.Confirming needs an MSIX install plus a real desktop session (.173/.221 were off-LAN). Two headless workarounds dead-ended and are not worth repeating:
Invoke-CommandInDesktopPackageinto a stock UWP package can't host a probe (AppContainer), andNew-SelfSignedCertificatewon't persist a key over ssh on .133, so a hand-rolled signed full-trust test package isn't buildable there either.