diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..d1b8800
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,53 @@
+name: Bug report
+description: Something in FieldKit did not work as expected.
+labels: [bug]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Every run writes a timestamped log. Attaching it makes a report far
+ easier to act on — use **Open Log** in the toolbar to find it.
+
+ - type: textarea
+ id: what-happened
+ attributes:
+ label: What happened
+ description: What you did, what you expected, and what you got instead.
+ validations:
+ required: true
+
+ - type: input
+ id: operation
+ attributes:
+ label: Which operation
+ description: The name shown in the operations list, if the problem was in one specific task.
+ placeholder: Clear Temporary Files
+ validations:
+ required: false
+
+ - type: input
+ id: version
+ attributes:
+ label: FieldKit version
+ description: Right-click FieldKit.exe, then Properties, then Details.
+ placeholder: 1.1.0
+ validations:
+ required: true
+
+ - type: input
+ id: windows
+ attributes:
+ label: Windows version
+ description: Run `winver`, or Settings, then System, then About.
+ placeholder: Windows 11 24H2 (26100.2314)
+ validations:
+ required: true
+
+ - type: textarea
+ id: log
+ attributes:
+ label: Log output
+ description: Paste the relevant part of the log, or attach the whole file.
+ render: text
+ validations:
+ required: false
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..9e977cc
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: true
+contact_links:
+ - name: Security vulnerability
+ url: https://github.com/tsudo/fieldkit/security/policy
+ about: Report privately. Do not open a public issue for a security problem.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..53a7c19
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,33 @@
+name: Feature request
+description: Suggest an operation or change for FieldKit.
+labels: [enhancement]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ [ROADMAP.md](https://github.com/tsudo/fieldkit/blob/main/ROADMAP.md)
+ covers what is already planned and what is deliberately out of scope.
+ Worth a look first.
+
+ - type: textarea
+ id: problem
+ attributes:
+ label: What problem does this solve
+ description: Describe the situation you run into, not the implementation you have in mind.
+ validations:
+ required: true
+
+ - type: textarea
+ id: proposal
+ attributes:
+ label: What you would like it to do
+ validations:
+ required: true
+
+ - type: textarea
+ id: alternatives
+ attributes:
+ label: How you handle it today
+ description: The manual steps, script, or other tool you currently use.
+ validations:
+ required: false
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..82119c4
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,20 @@
+## What this changes
+
+
+
+## Why
+
+
+
+## How it was verified
+
+
+
+- [ ] `dotnet build FieldKit.sln --configuration Release` succeeds
+- [ ] Ran the affected operation on Windows 10 or 11
+- [ ] No new warnings
+
+## Notes for the reviewer
+
+
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index fbbad3c..9da2e3c 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -5,3 +5,11 @@ updates:
schedule:
interval: "weekly"
open-pull-requests-limit: 5
+
+ # The workflows pin actions by major tag; without this ecosystem those pins
+ # get no update PRs and drift silently.
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 5
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d3b7074..634bd10 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -6,12 +6,53 @@ on:
pull_request:
branches: [main]
+permissions:
+ contents: read
+
jobs:
build:
runs-on: windows-latest
steps:
- - uses: actions/checkout@v5
- - uses: actions/setup-dotnet@v4
+ - uses: actions/checkout@v7
+ - uses: actions/setup-dotnet@v6
with:
- dotnet-version: "9.0.x"
+ # Latest 10.0 SDK. global.json sets the floor; the SDK in use
+ # determines which runtime version gets embedded in the
+ # self-contained exe, so tracking forward here is deliberate.
+ dotnet-version: "10.0.x"
+
- run: dotnet build FieldKit.sln --configuration Release
+
+ # Verify the configuration that actually ships. `dotnet build` does not
+ # exercise single-file packing, self-contained runtime bundling, or
+ # compression — all of which have their own failure modes.
+ - name: Verify release publish
+ run: dotnet publish FieldKit/FieldKit.csproj --configuration Release --output publish
+
+ - name: Confirm single-file artifact
+ shell: pwsh
+ run: |
+ $exe = "publish/FieldKit.exe"
+ if (-not (Test-Path $exe)) { throw "Expected single-file artifact at $exe" }
+
+ # A size floor alone does not prove single-file: dropping
+ # IncludeNativeLibrariesForSelfExtract still yields a ~59 MB exe but
+ # scatters native DLLs beside it, which an elevated binary would then
+ # resolve from its launch directory. Assert nothing else is emitted.
+ $stray = Get-ChildItem publish -File |
+ Where-Object { $_.Name -ne 'FieldKit.exe' -and $_.Extension -ne '.pdb' }
+ if ($stray) { throw "Not single-file - loose files present: $($stray.Name -join ', ')" }
+
+ $sizeMb = [math]::Round((Get-Item $exe).Length / 1MB, 1)
+ Write-Host "FieldKit.exe = $sizeMb MB"
+ if ($sizeMb -lt 40) { throw "Artifact is $sizeMb MB - self-contained runtime looks missing" }
+ if ($sizeMb -gt 100) { throw "Artifact is $sizeMb MB - compression looks disabled" }
+
+ # Retain it: without this the artifact that just passed verification is
+ # destroyed with the runner, so a failed assertion leaves nothing to
+ # inspect and the exe is never smoke-tested off the author's machine.
+ - uses: actions/upload-artifact@v7
+ with:
+ name: FieldKit-exe
+ path: publish/FieldKit.exe
+ if-no-files-found: error
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..6eae4c2
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,59 @@
+name: CodeQL
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+ schedule:
+ # Weekly, so advisories that land after a quiet period still get caught.
+ - cron: "23 5 * * 1"
+
+jobs:
+ analyze:
+ name: Analyze ${{ matrix.language }}
+ runs-on: windows-latest
+ permissions:
+ security-events: write
+ actions: read
+ contents: read
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ # Manual build mode rather than buildless: FieldKit already has a
+ # known-good build, and compiling gives CodeQL full type resolution
+ # across the partial classes WPF generates from XAML.
+ - language: csharp
+ build-mode: manual
+ # The workflows in this repo are themselves analyzable.
+ - language: actions
+ build-mode: none
+
+ steps:
+ - uses: actions/checkout@v7
+
+ - uses: actions/setup-dotnet@v6
+ if: matrix.language == 'csharp'
+ with:
+ # Latest 10.0 SDK, matching build.yml. Resolving from global.json
+ # instead would freeze the embedded self-contained runtime at
+ # whatever that SDK bundles — and a self-contained app only gets
+ # runtime CVE fixes by being rebuilt.
+ dotnet-version: "10.0.x"
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v4
+ with:
+ languages: ${{ matrix.language }}
+ build-mode: ${{ matrix.build-mode }}
+
+ - name: Build
+ if: matrix.language == 'csharp'
+ run: dotnet build FieldKit.sln --configuration Release
+
+ - name: Perform CodeQL analysis
+ uses: github/codeql-action/analyze@v4
+ with:
+ category: "/language:${{ matrix.language }}"
diff --git a/.gitignore b/.gitignore
index 2789d71..94b2bc4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
bin/
obj/
+publish/
*.user
*.suo
.vs/
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..98e91e6
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,71 @@
+# Changelog
+
+All notable changes to FieldKit are recorded here.
+
+The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
+this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [1.1.0] - 2026-08-15
+
+Maintenance release. No new operations — this moves the runtime off a version
+heading for end of support and fixes two defects found during review.
+
+### Changed
+
+- Runtime moved from .NET 9 to .NET 10 (LTS). .NET 9 reaches end of support on
+ 2026-11-10; because FieldKit publishes as a self-contained single file, the
+ runtime is embedded in the exe, so shipping on .NET 9 past that date would
+ hand every downloader an unpatched runtime. .NET 10 is supported through
+ 2028-11-14.
+- `System.Management` and `System.ServiceProcess.ServiceController` updated from
+ 9.0.0 to 10.0.11.
+- `global.json` sets .NET 10 as the minimum SDK, so a build on an older SDK
+ fails immediately with a clear message. It is a floor rather than a pin by
+ design: the SDK in use determines which runtime version is embedded in the
+ self-contained exe, and a self-contained app only receives runtime fixes when
+ it is rebuilt, so tracking forward is the safer default.
+
+### Fixed
+
+- Removed a duplicate `Logger` instance. `MainWindow`'s parameterless
+ constructor created its own logger, and the real constructor chained through
+ it before replacing the field, leaving an undisposed `StreamWriter` behind.
+ The visible symptom was an empty `FieldKit-*.log` file appearing next to the
+ exe on every launch.
+- Unhandled exceptions on the UI thread are now caught, written to the log, and
+ reported with the log path, instead of closing the app without explanation.
+- A run that stops early now says so. Previously the status bar read "Run
+ complete" at 100% whether or not the run finished; an interrupted run reports
+ "Run aborted", marks the operations that did not finish, and shows what went
+ wrong instead of a completion summary.
+- Closing the window mid-run no longer disposes the cancellation token while the
+ run loop is still using it. Every remaining operation used to fail with
+ `ObjectDisposedException`, which read in the log as many broken operations
+ rather than one lifecycle bug.
+
+### Added
+
+- CodeQL static analysis, running on every push and pull request to `main` plus
+ a weekly schedule.
+- Continuous integration now verifies the published single-file artifact, not
+ just `dotnet build`. Single-file packing, self-contained runtime bundling, and
+ compression have their own failure modes that a plain build never exercises.
+- Issue and pull request templates.
+- This changelog.
+
+## [1.0.0] - 2026-04-06
+
+First release.
+
+### Added
+
+- 12 maintenance operations across cleanup, updates, and system health.
+- Presets for focused runs: All, Cleanup, Updates, Health, Defaults.
+- WPF interface with a Fluent dark theme.
+- System Information panel with export.
+- Timestamped log file for every run.
+- Security hardening: absolute paths for system binaries, and symlink and
+ junction protection on temp cleanup.
+
+[1.1.0]: https://github.com/tsudo/fieldkit/releases/tag/v1.1.0
+[1.0.0]: https://github.com/tsudo/fieldkit/releases/tag/v1.0.0
diff --git a/FieldKit/App.xaml.cs b/FieldKit/App.xaml.cs
index 6857d30..8e630b3 100644
--- a/FieldKit/App.xaml.cs
+++ b/FieldKit/App.xaml.cs
@@ -1,4 +1,5 @@
using System.Windows;
+using System.Windows.Threading;
using FieldKit.Services;
using FieldKit.Tasks;
@@ -44,6 +45,12 @@ private void App_Startup(object sender, StartupEventArgs e)
var mainWindow = new MainWindow(tasks, _logger);
mainWindow.Show();
+
+ // Registered only once startup has succeeded. Anything that fails
+ // before this point belongs to the catch below, which shuts down
+ // deliberately — including failures inside the disclaimer dialog,
+ // whose whole job is obtaining consent before destructive work.
+ DispatcherUnhandledException += OnDispatcherUnhandledException;
}
catch (Exception ex)
{
@@ -53,6 +60,27 @@ private void App_Startup(object sender, StartupEventArgs e)
}
}
+ ///
+ /// Last-chance handler for exceptions raised on the UI thread after startup.
+ /// Maintenance tasks catch their own failures and the run loop reports its
+ /// own aborts, so anything reaching here is a UI-layer bug outside a run.
+ /// The app stays alive so the user keeps access to the log path rather than
+ /// losing it to a silent crash.
+ ///
+ private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
+ {
+ var logger = _logger;
+ logger?.Log($"Unhandled UI exception: {e.Exception}", "ERROR");
+
+ var logNote = logger is null ? "" : $"\n\nLog: {logger.LogFilePath}";
+ MessageBox.Show(
+ $"FieldKit hit an unexpected error:\n\n{e.Exception.Message}{logNote}",
+ "FieldKit — Unexpected Error",
+ MessageBoxButton.OK, MessageBoxImage.Error);
+
+ e.Handled = true;
+ }
+
protected override void OnExit(ExitEventArgs e)
{
_logger?.Dispose();
diff --git a/FieldKit/FieldKit.csproj b/FieldKit/FieldKit.csproj
index d342d5e..bf9fc57 100644
--- a/FieldKit/FieldKit.csproj
+++ b/FieldKit/FieldKit.csproj
@@ -2,7 +2,7 @@
WinExe
- net9.0-windows
+ net10.0-windows
enable
true
enable
@@ -10,7 +10,7 @@
fieldkit.ico
FieldKit
FieldKit
- 1.0.0
+ 1.1.0
Keith S. Crawford
FieldKit
Portable Windows maintenance in a single exe. No installer, no bloat, no telemetry.
@@ -23,8 +23,8 @@
-
-
+
+
diff --git a/FieldKit/MainWindow.xaml.cs b/FieldKit/MainWindow.xaml.cs
index 311fb19..9518849 100644
--- a/FieldKit/MainWindow.xaml.cs
+++ b/FieldKit/MainWindow.xaml.cs
@@ -15,15 +15,9 @@ public partial class MainWindow : Window
private readonly ObservableCollection _taskViewModels = new();
private CancellationTokenSource? _runCts;
- public MainWindow()
+ public MainWindow(IReadOnlyList tasks, Logger logger)
{
InitializeComponent();
- _tasks = Array.Empty();
- _logger = new Logger();
- }
-
- public MainWindow(IReadOnlyList tasks, Logger logger) : this()
- {
_tasks = tasks;
_logger = logger;
@@ -37,7 +31,10 @@ public MainWindow(IReadOnlyList tasks, Logger logger) : this()
Closed += (_, _) =>
{
_logger.LogWritten -= OnLogWritten;
- _runCts?.Dispose();
+ // Cancel, don't dispose — the run loop is still reading this token
+ // and owns disposal in its finally. Disposing here made every
+ // remaining operation fail with ObjectDisposedException.
+ _runCts?.Cancel();
};
UpdateSummary();
@@ -176,6 +173,7 @@ private async Task RunSelectedTasksAsync()
SetRunningState(true);
_logger.Log($"Run started. Preview: {(dryRun ? "ON" : "OFF")}. Log: {_logger.LogFilePath}", "INFO");
+ var completed = false;
try
{
for (int i = 0; i < selected.Count; i++)
@@ -215,18 +213,36 @@ private async Task RunSelectedTasksAsync()
RefreshDetails();
}
+
+ completed = true;
+ }
+ catch (Exception ex)
+ {
+ // The run owns its own failure. Letting this reach the global
+ // dispatcher handler would leave the status bar reading
+ // "Run complete" on a run that stopped halfway through.
+ _logger.Log($"Run aborted: {ex}", "ERROR");
+
+ foreach (var vm in selected.Where(vm => vm.Status == "Running"))
+ vm.Status = "Aborted";
+
+ MessageBox.Show(this,
+ $"The run stopped early and did not finish.\n\n{ex.Message}\n\nLog: {_logger.LogFilePath}",
+ "FieldKit — Run Aborted", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
- ProgressBar.Value = 100;
- StatusText.Text = "Run complete";
+ ProgressBar.Value = completed ? 100 : 0;
+ StatusText.Text = completed ? "Run complete" : "Run aborted";
SetRunningState(false);
_runCts.Dispose();
_runCts = null;
+ RefreshDetails();
}
- ShowCompletionSummary(dryRun, success, warning, error, skipped,
- startFree, TryGetSystemDriveFreeSpace(), rebootRecommended);
+ if (completed)
+ ShowCompletionSummary(dryRun, success, warning, error, skipped,
+ startFree, TryGetSystemDriveFreeSpace(), rebootRecommended);
}
private void ShowCompletionSummary(bool dryRun, int success, int warning, int error, int skipped,
diff --git a/FieldKit/app.manifest b/FieldKit/app.manifest
index 1e04c29..d311e66 100644
--- a/FieldKit/app.manifest
+++ b/FieldKit/app.manifest
@@ -1,6 +1,6 @@
-
+
diff --git a/README.md b/README.md
index 092833d..eedabe4 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@



-
+
Portable Windows maintenance in a single exe. No installer, no bloat, no telemetry.
@@ -11,7 +11,7 @@ FieldKit runs cleanup, updates, and system health checks from one window. Pick a
## Download
-Grab the latest `FieldKit.exe` from the [Releases](https://github.com/tsudo/fieldkit/releases) page.
+Grab the latest `FieldKit.exe` from the [Releases](https://github.com/tsudo/fieldkit/releases) page. See the [changelog](CHANGELOG.md) for what changed.
- Windows 10 or 11, 64-bit
- No installer — run it from anywhere
@@ -110,7 +110,7 @@ FieldKit reports these cases clearly in the status column and log instead of fai
## Building From Source
-Requires the [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0).
+Requires the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0). `global.json` sets `10.0.100` as the minimum; the build uses the newest .NET 10 SDK you have installed.
```bash
dotnet build FieldKit.sln
@@ -119,10 +119,10 @@ dotnet build FieldKit.sln
To publish a portable single-file exe:
```bash
-dotnet publish FieldKit.sln -c Release
+dotnet publish FieldKit/FieldKit.csproj -c Release -o publish
```
-Output: `FieldKit/bin/Release/net9.0-windows/win-x64/publish/FieldKit.exe`
+Output: `publish/FieldKit.exe`
## Privacy
diff --git a/ROADMAP.md b/ROADMAP.md
index 93e978c..09b25c6 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -99,7 +99,7 @@ That means the user experience should answer:
## UI Direction
-v1.0 migrated from WinForms to WPF with .NET 9 Fluent dark theme.
+v1.0 migrated from WinForms to WPF with a Fluent dark theme. v1.1 moved the runtime to .NET 10 (LTS).
Current state:
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..28820ed
--- /dev/null
+++ b/global.json
@@ -0,0 +1,7 @@
+{
+ "//": "Minimum SDK, not a pin. rollForward latestFeature uses the newest .NET 10 SDK installed, which keeps the embedded self-contained runtime current on rebuild.",
+ "sdk": {
+ "version": "10.0.100",
+ "rollForward": "latestFeature"
+ }
+}