Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/knowledgebase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Use these when citing findings in plans, PRs, or audits:

- [Install lifecycle](install-lifecycle.md) — wizard page order, `InstallationService`, checkpoints, widescreen, CLI flags
- [Download system](download-system.md) — ResourceRegistry, handler order, `DownloadCacheService`, GUI vs CLI
- [Mod update checking](update-checking.md) — `NexusApiClient`, `ResourceMetadata` version fields, `ModUpdateCheckService`
- [Mod update checking](update-checking.md) — `NexusApiClient`, `ModUpdateCheckService`, mod list update badges, menu check action
- [Install profiles](install-profiles.md) — named loadouts, `ProfileService` capture/apply, `ProfileManagerDialog`
- [Managed deployment engine](managed-deployment.md) — hardlink deploy, per-component manifests, uninstall/purge (Phase 4 slice 1)
- [File-level conflicts](file-conflicts.md) — `FileConflictAnalyzer` dry-run VFS attribution and `ConflictsDialog` results UI
Expand Down Expand Up @@ -84,7 +84,7 @@ Use these when citing findings in plans, PRs, or audits:

### Plans (implementation history)

- [docs/plans/](../plans/) — dated plans with requirements and status frontmatter
- [docs/plans/](../plans/) — dated plans with requirements and status frontmatter; active parity tracker: [vortex-mo2-feature-parity-living-plan.md](../plans/vortex-mo2-feature-parity-living-plan.md)

#### June 2026 arcs (rebrand closure + MainWindow extraction)

Expand Down
7 changes: 5 additions & 2 deletions docs/knowledgebase/update-checking.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Mod update checking

How ModSync knows whether a tracked Nexus Mods component has a newer version. Core-only as of plan 117; no GUI surface yet.
How ModSync knows whether a tracked Nexus Mods component has a newer version available.

## Components

Expand All @@ -9,6 +9,9 @@ How ModSync knows whether a tracked Nexus Mods component has a newer version. Co
| `NexusApiClient` | `src/ModSync.Core/Services/Download/NexusApiClient.cs` | `[REPO]` Standalone typed client for the Nexus REST API (`/games/{domain}/mods/{id}.json`, `files.json`, `md5_search`, `/users/validate.json`). Handles 429 Retry-After (single retry) and tracks `X-RL-Daily-Remaining` / `X-RL-Hourly-Remaining` as `DailyRemaining` / `HourlyRemaining`. |
| `ResourceMetadata` version fields | `src/ModSync.Core/ModComponent.cs` | `[REPO]` `ModVersion` (baseline), `LatestKnownVersion`, `LastUpdateCheck`, `UpdateAvailable` — plain serializable properties on each `ResourceRegistry` entry. |
| `ModUpdateCheckService` | `src/ModSync.Core/Services/ModUpdateCheckService.cs` | `[REPO]` Batch checker: extracts `(gameDomain, modId)` from `nexusmods.com/{domain}/mods/{id}` URLs in each component's `ResourceRegistry`, fetches mod info once per unique mod across all components, writes results onto the metadata, stops early when the rate-limit budget hits zero. |
| `ModComponent.NexusUpdateAvailable` | `src/ModSync.Core/ModComponent.cs` | `[REPO]` Computed flag for list binding; call `NotifyNexusUpdateStateChanged()` after mutating metadata. |
| Mod list badge | `src/ModSync.GUI/Controls/ModListItem.axaml` | `[REPO]` "Update" badge when `NexusUpdateAvailable` is true. |
| Menu action | `src/ModSync.GUI/Services/MenuBuilderService.cs` | `[REPO]` **Check for Nexus Updates** runs the batch checker (requires API key) and refreshes badges. |

## Semantics

Expand All @@ -22,7 +25,7 @@ How ModSync knows whether a tracked Nexus Mods component has a newer version. Co

- `ModComponent.ResourceRegistry` getter returns a defensive copy of the dictionary, but the `ResourceMetadata` values are shared references; the service mutates them in place — that is the supported persistence path. `[REPO]`
- `ModSync.Core` compiles as C# 7.3 and multi-targets net48; keep additions to these classes within that language level. `[REPO]`
- `NexusModsDownloadHandler` still has its own inline API code; migrating it onto `NexusApiClient` is a planned follow-up, as are endorsements and GUI update badges (see `docs/plans/2026-06-12-117-nexus-update-checks-plan.md`). `[REPO]`
- `NexusModsDownloadHandler` still has its own inline API code; migrating it onto `NexusApiClient` is a planned follow-up, as are endorsements and persisting check results via `DownloadCacheService`. `[REPO]`

## Tests

Expand Down
37 changes: 37 additions & 0 deletions docs/plans/2026-06-14-122-update-badges-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
title: "feat: Nexus update badges and check-for-updates menu action"
type: feat
status: completed
date: 2026-06-14
origin: docs/plans/2026-06-12-117-nexus-update-checks-plan.md (Phase 2, GUI slice)
branch: feat/update-badges-gui
---

# feat: Nexus update badges and check-for-updates menu action

Second sub-slice of Phase 2 (mod version tracking + update checks) of the Vortex/MO2
feature-parity roadmap.

## Goal

Surface `ModUpdateCheckService` results in the mod list and expose a user-triggered
"Check for Nexus Updates" action without changing download or install pipelines.

## Landed

- `ModComponent.NexusUpdateAvailable` + `NotifyNexusUpdateStateChanged()` for list binding.
- `ModListItem` "Update" badge when any tracked Nexus resource has `UpdateAvailable`.
- `MenuBuilderService` common menu item runs `ModUpdateCheckService` and refreshes badges.
- Knowledgebase and living-plan delta updated.

## Out of scope

- Sidebar-only badges, endorsement UI, persisting check results via `DownloadCacheService`.
- Migrating `NexusModsDownloadHandler` onto `NexusApiClient`.
- Desktop GUI validation of badge rendering (headless agent skipped).

## Tests

```bash
dotnet test src/ModSync.Tests/ModSync.Tests.csproj --filter "FullyQualifiedName~ModUpdateCheck"
```
50 changes: 50 additions & 0 deletions docs/plans/vortex-mo2-feature-parity-living-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
title: "Vortex/MO2 feature parity — living plan"
type: living-plan
status: active
date: 2026-06-14
origin: Vortex/MO2 feature-parity roadmap (phases 1–7)
---

# Vortex/MO2 feature parity — living plan

Single authoritative tracker for parity work. Individual slice plans under
`docs/plans/` remain the implementation record; this file holds status and next steps only.

## Phase status

| Phase | Area | Status |
|-------|------|--------|
| 1 | nxm protocol handler | Merged (#155–#164) |
| 2 | Nexus update checks | Core merged (#156); GUI badges in PR #167 (plan 122) |
| 3 | Profiles | Merged (#157) |
| 4 | Managed deployment | **Merged** (#158); install wiring deferred |
| 5 | File conflicts | Core #160 + GUI #165 merged |
| 6 | FOMOD | Parser + installer dialog merged (#166); archive hook deferred |
| 7 | (roadmap tail) | Per slice plans |

## Delta update (2026-06-14)

### Landed

- FOMOD installer dialog + parser stack (#166).
- Conflicts analysis GUI (#165).
- Nexus update check core (#156).
- **Update badges + "Check for Nexus Updates" menu action** (plan 122, branch `feat/update-badges-gui`).

### Partial

- Managed deployment engine (#158): rebased on master, CI pending → merge when green; no install-path wiring yet.
- FOMOD: no automatic archive enumeration hook in download flow.
- Update checking: no endorsement UI, no cache persistence of check results.

### Next

1. Merge PR #158 when CI is green.
2. Wire `DeploymentService` into install execution + optional GUI toggle (plan 116 slice 2).
3. FOMOD archive discovery hook in download/archive enumeration.
4. Migrate `NexusModsDownloadHandler` to `NexusApiClient` when download handler branch is stable.

## Superseded

- PR #162 closed; superseded by #166 (FOMOD dialog includes parser stack).
25 changes: 25 additions & 0 deletions src/ModSync.Core/ModComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,31 @@ public bool IsValidating
OnPropertyChanged();
}
}

/// <summary>
/// True when any <see cref="ResourceRegistry"/> entry has
/// <see cref="ResourceMetadata.UpdateAvailable"/> set after a Nexus update check.
/// </summary>
[JsonIgnore]
public bool NexusUpdateAvailable
{
get
{
foreach (ResourceMetadata metadata in _resourceRegistry.Values)
{
if (metadata != null && metadata.UpdateAvailable)
{
return true;
}
}

return false;
}
}

/// <summary>Raises <see cref="PropertyChanged"/> for <see cref="NexusUpdateAvailable"/> after metadata mutation.</summary>
public void NotifyNexusUpdateStateChanged() => OnPropertyChanged(nameof(NexusUpdateAvailable));

public bool WidescreenOnly
{
get => _widescreenOnly;
Expand Down
29 changes: 24 additions & 5 deletions src/ModSync.GUI/Controls/ModListItem.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
<!-- Main mod info (always visible) -->
<Grid x:Name="MainModInfo" Grid.Row="0" RowDefinitions="Auto,Auto">
<!-- Row 0: main info across 6 columns -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto">
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto,Auto">

<!-- Checkbox -->
<CheckBox
Expand Down Expand Up @@ -86,19 +86,38 @@
</TextBlock.Text>
</TextBlock>

<!-- Nexus update badge -->
<Border
Grid.Column="3"
Margin="8,0,4,0"
Padding="6,2"
VerticalAlignment="Top"
Classes="mod-list-item-badge"
CornerRadius="6"
IsVisible="{Binding NexusUpdateAvailable}"
ToolTip.Tip="A newer version is available on Nexus Mods">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="10"
FontWeight="Medium"
Text="Update"
TextTrimming="CharacterEllipsis" />
</Border>

<!-- Validation Status Icon -->
<TextBlock
x:Name="ValidationIcon"
Grid.Column="3"
Margin="8,0,8,0"
Grid.Column="4"
Margin="4,0,8,0"
VerticalAlignment="Top"
FontSize="14"
IsVisible="False" />

<!-- Drag Handle (Editor Mode Only) -->
<TextBlock
x:Name="DragHandle"
Grid.Column="4"
Grid.Column="5"
VerticalAlignment="Center"
Classes="mod-list-item-drag-handle"
Cursor="Hand"
Expand All @@ -111,7 +130,7 @@
<!-- Download Button -->
<Button
x:Name="DownloadButton"
Grid.Column="5"
Grid.Column="6"
Width="24"
Height="24"
Margin="4,0,0,0"
Expand Down
60 changes: 60 additions & 0 deletions src/ModSync.GUI/Services/MenuBuilderService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

using Avalonia.Controls;
Expand All @@ -13,6 +15,7 @@

using ModSync.Core;
using ModSync.Core.Services;
using ModSync.Core.Services.Download;
using ModSync.Dialogs;

using ReactiveUI;
Expand Down Expand Up @@ -344,6 +347,63 @@ await InformationDialog.ShowInformationDialogAsync(
}
}),
});

_ = items.Add(new MenuItem
{
Header = "🔔 Check for Nexus Updates",
Command = ReactiveCommand.CreateFromTask(RunNexusUpdateCheckAsync),
});
}

private async Task RunNexusUpdateCheckAsync()
{
if (string.IsNullOrWhiteSpace(MainConfig.NexusModsApiKey))
{
await InformationDialog.ShowInformationDialogAsync(
_parentWindow,
"Set a Nexus Mods API key in Settings before checking for updates.");
return;
}

List<ModComponent> mods = _modManagementService.SearchMods(string.Empty);
using (var httpClient = new HttpClient())
{
var apiClient = new NexusApiClient(httpClient, MainConfig.NexusModsApiKey);
var updateService = new ModUpdateCheckService(apiClient);
ModUpdateCheckResult result = await updateService.CheckForUpdatesAsync(mods).ConfigureAwait(true);

foreach (ModComponent component in mods)
{
component.NotifyNexusUpdateStateChanged();
}

string summary = string.Format(
CultureInfo.InvariantCulture,
"Nexus update check complete.\n\nChecked: {0}\nSkipped (non-Nexus): {1}\nUpdates found: {2}",
result.CheckedCount,
result.SkippedCount,
result.UpdatesFound.Count);

if (result.RateLimitReached)
{
summary += "\n\nStopped early: Nexus API rate limit reached.";
}

if (result.Errors.Count > 0)
{
summary += string.Format(
CultureInfo.InvariantCulture,
"\n\nErrors ({0}):\n{1}",
result.Errors.Count,
string.Join("\n", result.Errors.Take(5)));
if (result.Errors.Count > 5)
{
summary += string.Format(CultureInfo.InvariantCulture, "\n… and {0} more.", result.Errors.Count - 5);
}
}

await InformationDialog.ShowInformationDialogAsync(_parentWindow, summary);
}
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "MA0051:Method is too long", Justification = "<Pending>")]
Expand Down
19 changes: 19 additions & 0 deletions src/ModSync.Tests/ModUpdateCheckServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,25 @@ public async Task CheckForUpdatesAsync_ApiError_RecordedAndOtherModsStillChecked
});
}

[Test]
public void NexusUpdateAvailable_ReflectsResourceMetadataFlags()
{
var metadataWithUpdate = new ResourceMetadata { UpdateAvailable = true };
var metadataWithoutUpdate = new ResourceMetadata { UpdateAvailable = false };
ModComponent withUpdate = CreateComponent(
"Has Update",
("https://www.nexusmods.com/kotor/mods/1", metadataWithUpdate));
ModComponent withoutUpdate = CreateComponent(
"No Update",
("https://www.nexusmods.com/kotor/mods/2", metadataWithoutUpdate));

Assert.Multiple(() =>
{
Assert.That(withUpdate.NexusUpdateAvailable, Is.True);
Assert.That(withoutUpdate.NexusUpdateAvailable, Is.False);
});
}

[Test]
public void CheckForUpdatesAsync_NullComponents_Throws()
{
Expand Down
Loading