4 Commits

Author SHA1 Message Date
justin 9bbb3b7449 v0.1.3
Release / build-installer (push) Has been cancelled
2026-05-08 11:31:09 -04:00
justin 32e07489ff Fix endpoint-row context menu: bindings via PlacementTarget.Tag
The ContextMenu lived in its own popup visual tree, so the menu items'
RelativeSource={RelativeSource AncestorType=Window} couldn't find the
Window and the bindings silently failed - none of Edit / Copy URL /
Toggle / Delete actually fired their commands.

Standard WPF workaround: park MainViewModel on each DataGridRow's Tag
(still in the Window's visual tree, so the row Setter binding resolves)
and reach it from the menu items via PlacementTarget.Tag. The toggle
command parameter likewise comes from PlacementTarget.DataContext (the
EndpointConfig the row represents).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:30:11 -04:00
justin 3cd8c94a94 Add File -> Minimize to tray toggle (default on)
Adds a checkable MenuItem so the user can opt out of the hide-to-tray
behavior. Persisted per-user to %APPDATA%\WebhookServer\gui.json so the
choice survives restarts.

When ticked (default): X / Alt+F4 / minimize hide to tray, GUI process
keeps running, tray icon persists.

When unticked: X actually closes the app, minimize is a regular
Windows minimize.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:28:53 -04:00
justin 2aa14642a0 GUI: hide-to-tray on X button; tray persists until explicit Exit
The minimize-to-tray behavior already worked, but clicking the X button
killed the GUI process and took the tray with it. That made "tray when
the GUI window is closed" a UX dead end - the only way to get the tray
was to leave the window minimized.

Now:
  - X button / Alt+F4 -> hide window, tray stays alive
  - Tray double-click -> reopens window
  - File -> Exit (or tray's Exit menu) -> truly quits the process

Wired by adding a RealExitRequested event on MainViewModel that the
window subscribes to (so File -> Exit sets the ExitForReal flag before
calling Shutdown), and a parallel onExit callback on TrayIcon for the
tray menu's Exit item. The Closing handler checks ExitForReal: if
false (X / Alt+F4) it cancels the close and hides; if true, it disposes
the tray and lets the close proceed.

Auto-start at login is still TBD - if you want the tray to be there
without manually launching the GUI after a reboot, that's a separate
Task Scheduler entry. Skipping for now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:26:49 -04:00
6 changed files with 4 additions and 120 deletions
-102
View File
@@ -1,102 +0,0 @@
name: Release (Gitea)
# Lives in .gitea/workflows/ so it runs on Gitea Actions only. The GitHub-side
# release lives in .github/workflows/release.yml.
#
# Triggered automatically on v* tag pushes; can also be invoked manually via
# workflow_dispatch with a version override (useful for testing the runner
# without bumping the project version).
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Version to build (e.g. 0.1.4). Defaults to Directory.Build.props.'
required: false
jobs:
build-installer:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Resolve version
id: ver
shell: pwsh
run: |
if ('${{ github.event_name }}' -eq 'push') {
$v = '${{ github.ref_name }}'.TrimStart('v')
} elseif ('${{ inputs.version }}') {
$v = '${{ inputs.version }}'
} else {
[xml]$p = Get-Content Directory.Build.props
$v = $p.Project.PropertyGroup.Version
}
"version=$v" | Out-File $env:GITHUB_OUTPUT -Append
Write-Host "Building version $v"
- name: Restore + test
shell: pwsh
run: |
dotnet restore WebhookServer.sln
dotnet test WebhookServer.sln -c Release
- name: Ensure Inno Setup is installed
shell: pwsh
run: |
if (-not (Get-Command iscc -ErrorAction SilentlyContinue) -and `
-not (Test-Path 'C:\Program Files (x86)\Inno Setup 6\ISCC.exe') -and `
-not (Test-Path 'C:\Program Files\Inno Setup 6\ISCC.exe')) {
choco install innosetup --no-progress -y
}
- name: Build installer
shell: pwsh
run: ./scripts/build-installer.ps1 -VersionOverride ${{ steps.ver.outputs.version }}
- name: Upload installer artifact
uses: actions/upload-artifact@v4
with:
name: WebhookServer-Setup-${{ steps.ver.outputs.version }}
path: dist/WebhookServer-Setup-*.exe
- name: Create Gitea release with installer attached
if: startsWith(github.ref, 'refs/tags/v')
shell: pwsh
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$version = '${{ steps.ver.outputs.version }}'
$tag = '${{ github.ref_name }}'
$repo = '${{ github.repository }}'
$serverUrl = '${{ github.server_url }}'
$apiBase = "$serverUrl/api/v1/repos/$repo"
$headers = @{ Authorization = "token $env:GITEA_TOKEN" }
# 1. Create the release.
$isPre = $version.StartsWith('0.')
$createBody = @{
tag_name = $tag
name = "Webhook Server $version"
body = "Automated build via Gitea Actions runner."
draft = $false
prerelease = $isPre
} | ConvertTo-Json
$rel = Invoke-RestMethod -Uri "$apiBase/releases" -Method Post `
-Headers $headers -ContentType 'application/json' -Body $createBody
Write-Host "Created release id=$($rel.id) tag=$tag"
# 2. Attach the installer.
$file = Get-Item "dist/WebhookServer-Setup-$version.exe"
$uploadUri = "$apiBase/releases/$($rel.id)/assets?name=$($file.Name)"
Invoke-RestMethod -Uri $uploadUri -Method Post -Headers $headers `
-ContentType 'application/octet-stream' -InFile $file.FullName | Out-Null
Write-Host "Uploaded $($file.Name) ($([math]::Round($file.Length / 1MB, 2)) MB) to $tag"
-1
View File
@@ -5,7 +5,6 @@ on:
branches: [main] branches: [main]
pull_request: pull_request:
branches: [main] branches: [main]
workflow_dispatch:
jobs: jobs:
build: build:
-3
View File
@@ -12,9 +12,6 @@ on:
jobs: jobs:
build-installer: build-installer:
# Gitea reads .github/workflows/ for compatibility, but the create-release
# step uses a GitHub-only action. Skip the whole job on non-GitHub runners.
if: github.server_url == 'https://github.com'
runs-on: windows-latest runs-on: windows-latest
permissions: permissions:
contents: write # needed to create releases / upload assets contents: write # needed to create releases / upload assets
-4
View File
@@ -11,10 +11,6 @@ on:
jobs: jobs:
sync: sync:
# Gitea reads .github/workflows/ for compatibility, but this workflow
# pushes to a GitHub-hosted wiki. Skip on non-GitHub runners; the Gitea
# wiki is synced separately via scripts/sync-wiki.ps1.
if: github.server_url == 'https://github.com'
runs-on: windows-latest runs-on: windows-latest
permissions: permissions:
contents: write contents: write
+1 -1
View File
@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>0.1.4</Version> <Version>0.1.3</Version>
<Authors>Justin Paul</Authors> <Authors>Justin Paul</Authors>
<Company>Justin Paul</Company> <Company>Justin Paul</Company>
<Product>Webhook Server</Product> <Product>Webhook Server</Product>
@@ -82,14 +82,8 @@ public sealed partial class ConfigCheckpointsViewModel : ObservableObject
{ {
if (Selected is null) return; if (Selected is null) return;
// Capture before the refresh; the ObservableCollection.Clear() in
// RefreshAsync nulls Selected (the original instance is gone from the
// collection so the SelectedItem binding clears).
var fileName = Selected.FileName;
var savedAt = Selected.SavedAt;
var ok = MessageBox.Show( var ok = MessageBox.Show(
$"Roll the configuration back to the checkpoint from {savedAt.ToLocalTime():yyyy-MM-dd HH:mm:ss}?\n\nThe current configuration is automatically saved as a new checkpoint first, so you can roll forward again.", $"Roll the configuration back to the checkpoint from {Selected.SavedAt.ToLocalTime():yyyy-MM-dd HH:mm:ss}?\n\nThe current configuration is automatically saved as a new checkpoint first, so you can roll forward again.",
"Confirm rollback", "Confirm rollback",
MessageBoxButton.OKCancel, MessageBoxButton.OKCancel,
MessageBoxImage.Warning); MessageBoxImage.Warning);
@@ -97,10 +91,10 @@ public sealed partial class ConfigCheckpointsViewModel : ObservableObject
try try
{ {
await _client.RestoreBackupAsync(fileName).ConfigureAwait(false); await _client.RestoreBackupAsync(Selected.FileName).ConfigureAwait(false);
await RefreshAsync().ConfigureAwait(false); await RefreshAsync().ConfigureAwait(false);
Application.Current.Dispatcher.Invoke(() => Application.Current.Dispatcher.Invoke(() =>
StatusMessage = $"Rolled back to {fileName}."); StatusMessage = $"Rolled back to {Selected!.FileName}.");
} }
catch (Exception ex) catch (Exception ex)
{ {