Fix chrome menu disappearing after 5 seconds#10
Conversation
ChannelInfo.cs introduces ReleaseChannel enum with per-branch constants for build expiry, feature unlock, telemetry defaults, logging level, and version suffix. Protected from forward merges via .gitattributes merge=ours. App.xaml.cs gains a kill switch (BuildExpiryDays), --reset flag for clean reinstall, and channel-aware Serilog minimum level. LicenseService bypasses validation when UnlockAllForTesting is set. SettingsService applies channel- aware telemetry default on fresh installs. BrandingInfo.Version bumped to 0.5.2. BuildDate constant added for CI stamping. FullVersion property composes display string from version, channel suffix, and build timestamp. CI updated: dev branch trigger, channel-aware version composition, BuildDate stamp step, and rolling-alpha job that keeps latest-alpha release current.
…piry Introduces ChannelInfo.cs (already committed) driving channel-specific behaviour across all three future branches (alpha/beta/stable). Changes in this commit: - BrandingInfo: Version → 0.5.2, adds BuildDate (CI-stamped), FullVersion (composed with channel suffix), DaysRemaining (countdown to expiry) - LicenseService: internal unlockForTesting constructor so tests can opt out of the alpha Personal-tier override independently of ChannelInfo - LicenseValidationTests: three tests updated to use unlockForTesting:false, fixing the CI build failure caused by UnlockAllForTesting=true on alpha - MainViewModel: IsPreRelease, ChannelBadgeText, BuildExpiryText properties for the drag strip watermark binding - MainWindow.xaml: ALPHA/BETA badge + days-remaining countdown in drag strip, right-aligned, hidden on stable builds via BoolToVis on IsPreRelease - README: dev/main build status badges, alpha/stable download channel table - CHANGELOG: [0.5.1-beta] promoted, [Unreleased] documents 0.5.2 additions
Replaces the 5-second DispatcherTimer auto-hide with a focus/hover model: - Mouse enters window → chrome visible (hover, unpinned) - Left-click anywhere → chrome pinned (stays until window loses focus) - Alt / Alt+letter → chrome pinned so WPF keyboard menu navigation works - Window loses focus (Deactivated) → chrome hidden, pin cleared - Mouse leaves without clicking → chrome hidden if not pinned Removes DispatcherTimer and using System.Windows.Threading entirely. No impact on hardware refresh — audio and COM port updates are WMI event-driven and have no dependency on the chrome visibility timer.
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Snapshot WarningsEnsure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice. Scanned FilesNone |
| } | ||
|
|
||
| // ── Build expiry check (alpha/beta only) ────────────────────────────── | ||
| if (ChannelInfo.BuildExpiryDays > 0 && !string.IsNullOrEmpty(BrandingInfo.BuildDate) |
Check warning
Code scanning / CodeQL
Constant condition Warning
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 days ago
In general, to fix a constant condition you either (1) remove the redundant constant part if it does not affect behavior, or (2) replace it with the correct, non-constant predicate that reflects the intended logic. Here, CodeQL indicates ChannelInfo.BuildExpiryDays > 0 is always true, so it does not influence whether the if block runs. The actual gating factors are !string.IsNullOrEmpty(BrandingInfo.BuildDate) and the success of DateTimeOffset.TryParse.
The best behavior‑preserving change is to simplify the condition by removing the constant subcondition and leaving the truly variable checks. That keeps the build-expiry feature working exactly as before, but avoids the misleading constant comparison and satisfies the analyzer. Concretely, in src/PortPane/App.xaml.cs, change the if at lines 34–37 to:
if (!string.IsNullOrEmpty(BrandingInfo.BuildDate)
&& DateTimeOffset.TryParse(BrandingInfo.BuildDate, null,
System.Globalization.DateTimeStyles.RoundtripKind, out var buildDate))
{
...
}No new methods, fields, or imports are required.
| @@ -31,7 +31,7 @@ | ||
| } | ||
|
|
||
| // ── Build expiry check (alpha/beta only) ────────────────────────────── | ||
| if (ChannelInfo.BuildExpiryDays > 0 && !string.IsNullOrEmpty(BrandingInfo.BuildDate) | ||
| if (!string.IsNullOrEmpty(BrandingInfo.BuildDate) | ||
| && DateTimeOffset.TryParse(BrandingInfo.BuildDate, null, | ||
| System.Globalization.DateTimeStyles.RoundtripKind, out var buildDate)) | ||
| { |
| } | ||
|
|
||
| // ── Build expiry check (alpha/beta only) ────────────────────────────── | ||
| if (ChannelInfo.BuildExpiryDays > 0 && !string.IsNullOrEmpty(BrandingInfo.BuildDate) |
Check warning
Code scanning / CodeQL
Constant condition Warning
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 days ago
In general, to fix a constant condition you either (1) remove the redundant part if it’s truly invariant, or (2) adjust the logic so it matches the intended behavior, ensuring that no sub-condition is trivially always true/false under normal operation.
Here, CodeQL says string.IsNullOrEmpty(BrandingInfo.BuildDate) is always true. Under that assumption, the current guard:
if (ChannelInfo.BuildExpiryDays > 0 && !string.IsNullOrEmpty(BrandingInfo.BuildDate)
&& DateTimeOffset.TryParse(...))
{
...
}has a middle term that is always false, so the whole if is never entered and the build-expiry feature is dead. The minimal behavior-preserving change is to remove the logically impossible check against BrandingInfo.BuildDate and rely solely on the TryParse call to decide whether a valid build date is available. DateTimeOffset.TryParse already handles null and empty strings safely (it just returns false), so the extra !string.IsNullOrEmpty is redundant in any case.
Concretely, in src/PortPane/App.xaml.cs around line 34, update the if condition to drop !string.IsNullOrEmpty(BrandingInfo.BuildDate) and keep only the expiry-days check plus the TryParse call. No additional imports, methods, or definitions are needed; we only adjust the condition expression.
| @@ -31,7 +31,7 @@ | ||
| } | ||
|
|
||
| // ── Build expiry check (alpha/beta only) ────────────────────────────── | ||
| if (ChannelInfo.BuildExpiryDays > 0 && !string.IsNullOrEmpty(BrandingInfo.BuildDate) | ||
| if (ChannelInfo.BuildExpiryDays > 0 | ||
| && DateTimeOffset.TryParse(BrandingInfo.BuildDate, null, | ||
| System.Globalization.DateTimeStyles.RoundtripKind, out var buildDate)) | ||
| { |
| { | ||
| get | ||
| { | ||
| if (string.IsNullOrEmpty(ChannelInfo.VersionSuffix)) |
Check warning
Code scanning / CodeQL
Constant condition Warning
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 days ago
General approach: normalize ChannelInfo.VersionSuffix into a local variable and base all conditions on that local, ensuring that the condition is not provably constant to the analyzer and clarifying the logic. This avoids removing any behavior while addressing the constant‑condition warning.
Concrete fix in src/PortPane/BrandingInfo.cs, property FullVersion (lines 25–38):
- At the top of the getter, introduce
var suffix = ChannelInfo.VersionSuffix ?? string.Empty;. - Replace
if (string.IsNullOrEmpty(ChannelInfo.VersionSuffix))withif (string.IsNullOrEmpty(suffix)). - In the final return, replace
$"{Version}-{ChannelInfo.VersionSuffix}"with$"{Version}-{suffix}".
No new imports or types are required; we use only string.Empty, which is already in System. This keeps behavior the same: if the suffix is null or empty, FullVersion returns Version. For alpha builds, we still generate the special alpha string; if that doesn’t apply, we append the normalized suffix.
| @@ -26,7 +26,8 @@ | ||
| { | ||
| get | ||
| { | ||
| if (string.IsNullOrEmpty(ChannelInfo.VersionSuffix)) | ||
| var suffix = ChannelInfo.VersionSuffix ?? string.Empty; | ||
| if (string.IsNullOrEmpty(suffix)) | ||
| return Version; | ||
|
|
||
| if (ChannelInfo.Channel == ReleaseChannel.Alpha && !string.IsNullOrEmpty(BuildDate) | ||
| @@ -34,7 +35,7 @@ | ||
| System.Globalization.DateTimeStyles.RoundtripKind, out var dt)) | ||
| return $"{Version}-alpha.{dt:yyyyMMdd}"; | ||
|
|
||
| return $"{Version}-{ChannelInfo.VersionSuffix}"; | ||
| return $"{Version}-{suffix}"; | ||
| } | ||
| } | ||
| /// <summary> |
| if (string.IsNullOrEmpty(ChannelInfo.VersionSuffix)) | ||
| return Version; | ||
|
|
||
| if (ChannelInfo.Channel == ReleaseChannel.Alpha && !string.IsNullOrEmpty(BuildDate) |
Check warning
Code scanning / CodeQL
Constant condition Warning
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 days ago
In general, to fix a constant condition you either (1) remove the redundant constant subcondition if it doesn’t affect behavior, or (2) refactor so that the condition can truly vary at runtime (for example, by not using a const/readonly value that is always the same). Here, we must not assume or change the behavior outside this snippet, so we should preserve the runtime behavior while eliminating the constant part that CodeQL flags.
Given that CodeQL believes ChannelInfo.Channel == ReleaseChannel.Alpha is always true, the behavior of FullVersion today is: if ChannelInfo.VersionSuffix is null/empty, return Version; otherwise, if BuildDate is non‑empty and parseable, return an alpha‑style string ${Version}-alpha.{yyyyMMdd}; otherwise, return ${Version}-{ChannelInfo.VersionSuffix}. Removing just the ChannelInfo.Channel == ReleaseChannel.Alpha && from the condition keeps exactly the same behavior under the current assumption that the channel is always Alpha, while eliminating the constant expression. So, in src/PortPane/BrandingInfo.cs, inside the FullVersion getter, change the if at lines 32–35 to drop the ChannelInfo.Channel == ReleaseChannel.Alpha && portion and leave the rest intact. No new imports or helpers are needed.
| @@ -29,7 +29,7 @@ | ||
| if (string.IsNullOrEmpty(ChannelInfo.VersionSuffix)) | ||
| return Version; | ||
|
|
||
| if (ChannelInfo.Channel == ReleaseChannel.Alpha && !string.IsNullOrEmpty(BuildDate) | ||
| if (!string.IsNullOrEmpty(BuildDate) | ||
| && DateTimeOffset.TryParse(BuildDate, null, | ||
| System.Globalization.DateTimeStyles.RoundtripKind, out var dt)) | ||
| return $"{Version}-alpha.{dt:yyyyMMdd}"; |
| if (string.IsNullOrEmpty(ChannelInfo.VersionSuffix)) | ||
| return Version; | ||
|
|
||
| if (ChannelInfo.Channel == ReleaseChannel.Alpha && !string.IsNullOrEmpty(BuildDate) |
Check warning
Code scanning / CodeQL
Constant condition Warning
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 days ago
In general, to fix this constant condition we must ensure BuildDate is not a compile‑time constant. Changing it from const to a runtime‑assigned value (e.g., static readonly or a normal static field) prevents the compiler from treating it as a constant and allows string.IsNullOrEmpty(BuildDate) to depend on the CI‑provided value as intended.
The single best, minimal‑behavior‑change fix here is:
- Change
public const string BuildDate = "";topublic static readonly string BuildDate = "";.
This preserves the public API surface (a static string field named BuildDate), keeps the default empty value in source, and allows the CI process or configuration system to override it without fighting constant inlining. After this change, string.IsNullOrEmpty(BuildDate) on line 32 (and line 49) will no longer be a constant expression, so the alpha/version and expiry logic will work as designed.
No new methods or special imports are needed; we only adjust the field declaration for BuildDate in src/PortPane/BrandingInfo.cs.
| @@ -15,7 +15,7 @@ | ||
| /// ISO 8601 UTC build timestamp. Empty string in source — patched by CI at | ||
| /// publish time. Used by App.xaml.cs to enforce ChannelInfo.BuildExpiryDays. | ||
| /// </summary> | ||
| public const string BuildDate = ""; | ||
| public static readonly string BuildDate = ""; | ||
|
|
||
| /// <summary> | ||
| /// Full version string for display, logging, and telemetry. |
|
|
||
| // ── Channel / build info (alpha/beta only) ──────────────────────────────── | ||
|
|
||
| public bool IsPreRelease => ChannelInfo.Channel != ReleaseChannel.Stable; |
Check warning
Code scanning / CodeQL
Comparison is constant Warning
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 days ago
In general, a “comparison is constant” issue should be fixed either by (a) changing the types/values so the comparison can be both true and false, if that’s the intended behavior, or (b) removing/replacing the comparison with a simpler expression that reflects the real invariant behavior. Here, CodeQL indicates that ChannelInfo.Channel != ReleaseChannel.Stable is always true under the project’s assumptions, so the IsPreRelease property is effectively “always true.” The safest fix that does not alter runtime behavior is to rewrite IsPreRelease so it clearly expresses the actual behavior without a redundant comparison.
The single best minimal-change fix is to replace ChannelInfo.Channel != ReleaseChannel.Stable with a direct constant true, accompanied by a comment explaining the pre-release-only nature of this build, or—better—express it as a positive check for the known channels used in this build (e.g., ChannelInfo.Channel is ReleaseChannel.Alpha or ReleaseChannel.Beta) if we want to keep some semantic link. However, since we must not assume unshown enum values and we must preserve existing behavior, the most reliable option is to set IsPreRelease to true unconditionally. That keeps the property’s public contract unchanged (it was always true at runtime per CodeQL) while eliminating the trivially true comparison.
Concretely, in src/PortPane/ViewModels/MainViewModel.cs, we will modify the IsPreRelease property declaration at line 93. No new imports, methods, or other definitions are required: it is a one-line change within the shown snippet.
| @@ -90,7 +90,7 @@ | ||
|
|
||
| // ── Channel / build info (alpha/beta only) ──────────────────────────────── | ||
|
|
||
| public bool IsPreRelease => ChannelInfo.Channel != ReleaseChannel.Stable; | ||
| public bool IsPreRelease => true; // This build only targets pre-release channels. | ||
| public string ChannelBadgeText => ChannelInfo.Channel == ReleaseChannel.Alpha ? "ALPHA" : "BETA"; | ||
| public string BuildExpiryText | ||
| { |
| // ── Channel / build info (alpha/beta only) ──────────────────────────────── | ||
|
|
||
| public bool IsPreRelease => ChannelInfo.Channel != ReleaseChannel.Stable; | ||
| public string ChannelBadgeText => ChannelInfo.Channel == ReleaseChannel.Alpha ? "ALPHA" : "BETA"; |
Check warning
Code scanning / CodeQL
Constant condition Warning
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 days ago
- In general, constant conditions should be removed or refactored so that the code explicitly reflects the only possible outcome instead of pretending to branch.
- Here, since the analyzer sees
ChannelInfo.Channelas always equal toReleaseChannel.Alpha, the ternary operator inChannelBadgeTextcan be simplified to just return"ALPHA". This preserves existing functionality (the expression already always evaluates to"ALPHA"in the analyzed configuration) and removes the constant condition. - Concretely, in
src/PortPane/ViewModels/MainViewModel.cs, replace the linepublic string ChannelBadgeText => ChannelInfo.Channel == ReleaseChannel.Alpha ? "ALPHA" : "BETA";withpublic string ChannelBadgeText => "ALPHA";. - No new methods, imports, or other definitions are needed; this is a simple expression replacement.
| @@ -91,7 +91,7 @@ | ||
| // ── Channel / build info (alpha/beta only) ──────────────────────────────── | ||
|
|
||
| public bool IsPreRelease => ChannelInfo.Channel != ReleaseChannel.Stable; | ||
| public string ChannelBadgeText => ChannelInfo.Channel == ReleaseChannel.Alpha ? "ALPHA" : "BETA"; | ||
| public string ChannelBadgeText => "ALPHA"; | ||
| public string BuildExpiryText | ||
| { | ||
| get |
| // ── Reset flag (wipes all user data and relaunches fresh) ───────────── | ||
| if (e.Args.Contains("--reset", StringComparer.OrdinalIgnoreCase)) | ||
| { | ||
| bool isPortable = File.Exists(Path.Combine(AppContext.BaseDirectory, "portable.txt")); |
Check notice
Code scanning / CodeQL
Call to 'System.IO.Path.Combine' may silently drop its earlier arguments Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 days ago
General fix: When composing paths where the intent is “base directory + relative subpath/file”, prefer Path.Join instead of Path.Combine. Path.Join does not treat later absolute segments in a way that discards earlier ones; it simply concatenates them with directory separators as needed. For cases where absolute paths are expected and must override earlier segments, Path.Combine may still be appropriate, but here we clearly want to append "portable.txt" to the base directory.
Best fix for this code: Replace the single usage of Path.Combine at line 54 with Path.Join, leaving the rest of the logic unchanged. This keeps functionality identical—AppContext.BaseDirectory plus "portable.txt"—but removes the specific CodeQL‑flagged pattern. No extra imports are needed because Path is in System.IO, which is already available in the base class library; and the file already references other System.* namespaces, so adding using System.IO; is optional for this edit (the existing code compiles because Path is fully qualified via System.IO.Path implicitly from mscorlib/System.Private.CoreLib). We only touch the shown snippet inside src/PortPane/App.xaml.cs.
Concretely:
- In
OnStartup, in the--resetblock, update:
bool isPortable = File.Exists(Path.Combine(AppContext.BaseDirectory, "portable.txt"));to:
bool isPortable = File.Exists(Path.Join(AppContext.BaseDirectory, "portable.txt"));No other code changes are required.
| @@ -51,7 +51,7 @@ | ||
| // ── Reset flag (wipes all user data and relaunches fresh) ───────────── | ||
| if (e.Args.Contains("--reset", StringComparer.OrdinalIgnoreCase)) | ||
| { | ||
| bool isPortable = File.Exists(Path.Combine(AppContext.BaseDirectory, "portable.txt")); | ||
| bool isPortable = File.Exists(Path.Join(AppContext.BaseDirectory, "portable.txt")); | ||
| string dataDir = isPortable | ||
| ? Path.Combine(AppContext.BaseDirectory, "PortPane-Data") | ||
| : Path.Combine( |
| { | ||
| bool isPortable = File.Exists(Path.Combine(AppContext.BaseDirectory, "portable.txt")); | ||
| string dataDir = isPortable | ||
| ? Path.Combine(AppContext.BaseDirectory, "PortPane-Data") |
Check notice
Code scanning / CodeQL
Call to 'System.IO.Path.Combine' may silently drop its earlier arguments Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 days ago
In general, to avoid Path.Combine silently discarding earlier segments when later segments may be absolute, you should either ensure all later segments are relative, or use Path.Join, which concatenates segments without treating absolute segments specially. Here, we want “base directory + fixed subfolder name” semantics, and the best fix is to use Path.Join instead of Path.Combine for those paths that are conceptually “base + child” and not meant to re-root.
Concretely, in src/PortPane/App.xaml.cs within the --reset handling block, change:
Path.Combine(AppContext.BaseDirectory, "PortPane-Data")toPath.Join(AppContext.BaseDirectory, "PortPane-Data").
This preserves the effective path today (because "PortPane-Data" is relative), but removes the risk of dropping the base directory if that string ever becomes absolute. No additional imports are required because Path.Join is in System.IO.Path, already in scope via System / System.IO being part of the BCL; the file already uses File and Directory, so the runtime environment clearly supports these APIs. Other Path.Combine calls in the snippet (those involving Environment.GetFolderPath and branding strings) are correctly using Combine to handle rooted paths and should remain unchanged.
| @@ -53,7 +53,7 @@ | ||
| { | ||
| bool isPortable = File.Exists(Path.Combine(AppContext.BaseDirectory, "portable.txt")); | ||
| string dataDir = isPortable | ||
| ? Path.Combine(AppContext.BaseDirectory, "PortPane-Data") | ||
| ? Path.Join(AppContext.BaseDirectory, "PortPane-Data") | ||
| : Path.Combine( | ||
| Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), | ||
| BrandingInfo.SuiteName, BrandingInfo.AppName); |
| : Path.Combine( | ||
| Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), | ||
| BrandingInfo.SuiteName, BrandingInfo.AppName); |
Check notice
Code scanning / CodeQL
Call to 'System.IO.Path.Combine' may silently drop its earlier arguments Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 days ago
In general, to avoid Path.Combine silently dropping earlier arguments when later ones are absolute, use Path.Join when you are concatenating path segments and you do not want absolute later segments to override earlier ones. Path.Join will concatenate segments using the directory separator without giving precedence to any argument.
For this specific case, replace the use of Path.Combine for dataDir’s non-portable path with Path.Join. This preserves the intended behavior for normal relative SuiteName and AppName values while preventing them from unexpectedly overriding the base %AppData% path if they were ever set to absolute paths. No additional imports are required because System.IO.Path is already available in the base class library.
Concretely:
- In
src/PortPane/App.xaml.cs, around lines 55–59, change:: Path.Combine(Environment.GetFolderPath(...), BrandingInfo.SuiteName, BrandingInfo.AppName);- to:
: Path.Join(Environment.GetFolderPath(...), BrandingInfo.SuiteName, BrandingInfo.AppName);
- Leave the other
Path.Combinecalls unchanged, as they combine a base directory with known, simple file or subdirectory names and are not part of the flagged issue.
| @@ -54,7 +54,7 @@ | ||
| bool isPortable = File.Exists(Path.Combine(AppContext.BaseDirectory, "portable.txt")); | ||
| string dataDir = isPortable | ||
| ? Path.Combine(AppContext.BaseDirectory, "PortPane-Data") | ||
| : Path.Combine( | ||
| : Path.Join( | ||
| Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), | ||
| BrandingInfo.SuiteName, BrandingInfo.AppName); | ||
| if (Directory.Exists(dataDir)) |
Summary
DispatcherTimerthat auto-hid the chrome menu barBehaviour change
What was checked
DispatcherTimerwas the only timer in the codebase; all hardware refresh (audio, COM ports) is WMI event-driven — no impactusing System.Windows.Threadingremoved (no longer needed)Test plan