Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f589c82
Add skipped entries counter to inventory monitor data
paul-fresquet Feb 19, 2026
7a816fd
Show local skipped entries count in identification view
paul-fresquet Feb 19, 2026
0ef175d
Add global skipped entries statistics to inventory status
paul-fresquet Feb 19, 2026
7d18cc2
fix: move inventories copy from property to method
paul-fresquet Apr 1, 2026
0b789de
test: fix ambiguous path matching in access handling integration tests
paul-fresquet Apr 1, 2026
d9038f9
test: stabilize periodic remaining-time emission assertion
paul-fresquet Apr 1, 2026
e62d768
test: remove Thread.Sleep usage and clear skipped queue directly
paul-fresquet Apr 1, 2026
6468750
refactor: address remaining sonar maintainability issues on PR
paul-fresquet Apr 1, 2026
3491157
refactor: make inventories accessors non-nullable
paul-fresquet Apr 1, 2026
e5c5fc0
refactor: remove redundant skipped counter field
paul-fresquet Apr 1, 2026
84704c3
fix: marshal inventory monitor updates to UI thread
paul-fresquet Apr 1, 2026
d08fc95
test: replace spin-waits with signal-based waits
paul-fresquet Apr 2, 2026
7da800a
feat: improve alignment & height
paul-fresquet Apr 2, 2026
173e736
feat: improve view
paul-fresquet Apr 2, 2026
580a273
refactor: always show local skipped entries with conditional badge em…
paul-fresquet Apr 2, 2026
d041a7e
refactor: always show global skipped entries with conditional badge e…
paul-fresquet Apr 2, 2026
9cbc0c4
feat: improve UI
paul-fresquet Apr 2, 2026
e79ec78
[fix] Align local inventory badges with border-label pattern
paul-fresquet Apr 2, 2026
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
6 changes: 6 additions & 0 deletions src/ByteSync.Client/Assets/Resources/Resources.fr.resx
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,9 @@ Voulez-vous continuer ?</value>
<data name="InventoryProcess_IdentificationErrors" xml:space="preserve">
<value>Erreurs d'identification :</value>
</data>
<data name="InventoryProcess_SkippedEntries" xml:space="preserve">
<value>Entrées ignorées :</value>
</data>
<data name="InventoryProcess_Start" xml:space="preserve">
<value>Démarrage :</value>
</data>
Expand Down Expand Up @@ -670,6 +673,9 @@ Voulez-vous continuer ?</value>
<data name="InventoryProcess_GlobalAnalyzeErrors" xml:space="preserve">
<value>Total des erreurs de calcul :</value>
</data>
<data name="InventoryProcess_GlobalSkippedEntries" xml:space="preserve">
<value>Total des entrées ignorées :</value>
</data>
<data name="InventoryProcess_GlobalIdentificationErrors" xml:space="preserve">
<value>Total erreurs d'identification :</value>
</data>
Expand Down
6 changes: 6 additions & 0 deletions src/ByteSync.Client/Assets/Resources/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,9 @@ Would you like to continue ?</value>
<data name="InventoryProcess_IdentificationErrors" xml:space="preserve">
<value>Identification Errors:</value>
</data>
<data name="InventoryProcess_SkippedEntries" xml:space="preserve">
<value>Skipped Entries:</value>
</data>
<data name="InventoryProcess_Start" xml:space="preserve">
<value>Start:</value>
</data>
Expand Down Expand Up @@ -679,6 +682,9 @@ Would you like to continue ?</value>
<data name="InventoryProcess_GlobalAnalyzeErrors" xml:space="preserve">
<value>Total Calculation Errors:</value>
</data>
<data name="InventoryProcess_GlobalSkippedEntries" xml:space="preserve">
<value>Total Skipped Entries:</value>
</data>
<data name="InventoryProcess_GlobalIdentificationErrors" xml:space="preserve">
<value>Total Identification Errors:</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public record InventoryMonitorData
public long UploadTotalVolume { get; set; }

public long UploadedVolume { get; set; }

public int SkippedEntriesCount { get; set; }

public bool HasNonZeroProperty()
{
Expand All @@ -36,6 +38,7 @@ public bool HasNonZeroProperty()
|| AnalyzableVolume != 0
|| IdentifiedVolume != 0
|| UploadTotalVolume != 0
|| UploadedVolume != 0;
|| UploadedVolume != 0
|| SkippedEntriesCount != 0;
}
}
18 changes: 7 additions & 11 deletions src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method maps InventoryBuildersto aList, but it returns nullwhenInventoryBuildersisnull. Is that intentional, or should it return an empty list instead? Also, are we sure every InventoryBuilderand itsInventory property are non-null here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks. I updated this to make the contract explicit and non-nullable:

  • InventoryProcessData.InventoryBuilders is now initialized to an empty list ([]) and is non-nullable.
  • GetInventories() now returns List<Inventory> (non-nullable) and returns an empty list when there are no builders.
  • Call sites in BaseInventoryRunner and FullInventoryRunner were updated to remove null-forgiving operators.

On the second point: IInventoryBuilder.Inventory is non-nullable in the interface and InventoryBuilder initializes it in its constructor, so we rely on that invariant here.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

L.150 : Why do we keep both _skippedCount and SkippedEntriesCount? Is one of them really necessary in addition to the other?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I simplified this.

_skippedCount was redundant, so I removed it and now SkippedCount is derived from _skippedCountsByReason.Values.Sum().

We still keep InventoryMonitorData.SkippedEntriesCount separately because it belongs to the observable monitor snapshot used by the UI stream, while SkippedCount/SkippedCountsByReason represent the process-level counters.

So there is now no duplicated private counter field in InventoryProcessData.

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ public class InventoryProcessData : ReactiveObject
private readonly object _monitorDataLock = new object();
private readonly ConcurrentQueue<SkippedEntry> _skippedEntries = new();
private readonly ConcurrentDictionary<SkipReason, int> _skippedCountsByReason = new();
private int _skippedCount;

public InventoryProcessData()
{
Expand Down Expand Up @@ -72,11 +71,11 @@ public InventoryProcessData()
Reset();
}

public List<IInventoryBuilder>? InventoryBuilders { get; set; }
public List<IInventoryBuilder> InventoryBuilders { get; set; } = [];

public List<Inventory>? Inventories
public List<Inventory> GetInventories()
{
get { return InventoryBuilders?.Select(ib => ib.Inventory).ToList(); }
return InventoryBuilders.Select(ib => ib.Inventory).ToList();
}

public CancellationTokenSource CancellationTokenSource { get; private set; }
Expand Down Expand Up @@ -107,7 +106,7 @@ public List<Inventory>? Inventories

public IReadOnlyCollection<SkippedEntry> SkippedEntries => _skippedEntries.ToArray();

public int SkippedCount => _skippedCount;
public int SkippedCount => _skippedCountsByReason.Values.Sum();

[Reactive]
public DateTimeOffset InventoryStart { get; set; }
Expand Down Expand Up @@ -146,7 +145,7 @@ public void RecordSkippedEntry(SkippedEntry entry)
{
_skippedEntries.Enqueue(entry);
_skippedCountsByReason.AddOrUpdate(entry.Reason, 1, (_, currentCount) => currentCount + 1);
Interlocked.Increment(ref _skippedCount);
UpdateMonitorData(m => { m.SkippedEntriesCount += 1; });
}

// should be used during issue 268 implementation
Expand Down Expand Up @@ -175,11 +174,8 @@ public void UpdateMonitorData(Action<InventoryMonitorData> action)

private void ClearSkippedEntries()
{
while (_skippedEntries.TryDequeue(out _))
{
}
_skippedEntries.Clear();

_skippedCountsByReason.Clear();
Interlocked.Exchange(ref _skippedCount, 0);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ public record InventoryStatistics
public int AnalyzeErrors { get; init; }

public int IdentificationErrors { get; init; }

public int TotalSkippedEntries { get; init; }
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like InventoryProcessDatanow stores InventoryBuilders instead of a direct Inventories list, so GetInventories() is used to derive the actual Inventory objects. Is that intentional? Also, should GetInventories() return an empty list instead ofnullto avoid using! here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is intentional.

InventoryProcessData keeps InventoryBuilders because the full-inventory phase needs more than the Inventory object itself (notably InventoryIndexer and SessionSettings).

I also applied your nullable-safety point:

  • InventoryBuilders is now non-nullable and initialized to []
  • GetInventories() now returns a non-null List<Inventory> (empty when there are no builders)
  • null-forgiving operators were removed at call sites

Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public async Task<bool> RunBaseInventory()
bool isOK;
try
{
await Parallel.ForEachAsync(InventoryProcessData.InventoryBuilders!,
await Parallel.ForEachAsync(InventoryProcessData.InventoryBuilders,
new ParallelOptions { MaxDegreeOfParallelism = 2, CancellationToken = InventoryProcessData.CancellationTokenSource.Token },
async (builder, token) =>
{
Expand All @@ -46,7 +46,7 @@ await Parallel.ForEachAsync(InventoryProcessData.InventoryBuilders!,
{
InventoryProcessData.IdentificationStatus.OnNext(InventoryTaskStatus.Success);

await _inventoryFinishedService.SetLocalInventoryFinished(InventoryProcessData.Inventories!, LocalInventoryModes.Base);
await _inventoryFinishedService.SetLocalInventoryFinished(InventoryProcessData.GetInventories(), LocalInventoryModes.Base);
}

isOK = true;
Expand All @@ -66,4 +66,4 @@ await Parallel.ForEachAsync(InventoryProcessData.InventoryBuilders!,

return isOK;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public async Task<bool> RunFullInventory()
await _sessionMemberService.UpdateCurrentMemberGeneralStatus(SessionMemberGeneralStatus.InventoryRunningAnalysis);

var inventoriesBuildersAndItems = new List<Tuple<IInventoryBuilder, HashSet<IndexedItem>>>();
foreach (var inventoryBuilder in InventoryProcessData.InventoryBuilders!)
foreach (var inventoryBuilder in InventoryProcessData.InventoryBuilders)
{
using var inventoryComparer =
_inventoryComparerFactory.CreateInventoryComparer(LocalInventoryModes.Base, inventoryBuilder.InventoryIndexer);
Expand Down Expand Up @@ -87,7 +87,7 @@ await Parallel.ForEachAsync(inventoriesBuildersAndItems,
else
{
InventoryProcessData.AnalysisStatus.OnNext(InventoryTaskStatus.Success);
await _inventoryFinishedService.SetLocalInventoryFinished(InventoryProcessData.Inventories!, LocalInventoryModes.Full);
await _inventoryFinishedService.SetLocalInventoryFinished(InventoryProcessData.GetInventories(), LocalInventoryModes.Full);
InventoryProcessData.MainStatus.OnNext(InventoryTaskStatus.Success);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ namespace ByteSync.Services.Inventories;

public class InventoryStatisticsService : IInventoryStatisticsService
{
private readonly IInventoryService _inventoryService;
private readonly IInventoryFileRepository _inventoryFileRepository;
private readonly ILogger<InventoryStatisticsService> _logger;

Expand All @@ -20,13 +19,12 @@ public class InventoryStatisticsService : IInventoryStatisticsService
public InventoryStatisticsService(IInventoryService inventoryService, IInventoryFileRepository inventoryFileRepository,
ILogger<InventoryStatisticsService> logger)
{
_inventoryService = inventoryService;
_inventoryFileRepository = inventoryFileRepository;
_logger = logger;

_statisticsSubject = new BehaviorSubject<InventoryStatistics?>(null);

_inventoryService.InventoryProcessData.AreFullInventoriesComplete
inventoryService.InventoryProcessData.AreFullInventoriesComplete
.DistinctUntilChanged()
.SelectMany(isComplete =>
isComplete
Expand Down Expand Up @@ -66,7 +64,8 @@ private void DoCompute()
ProcessedVolume = statsCollector.ProcessedSize,
AnalyzeSuccess = statsCollector.Success,
AnalyzeErrors = statsCollector.Errors,
IdentificationErrors = statsCollector.IdentificationErrors
IdentificationErrors = statsCollector.IdentificationErrors,
TotalSkippedEntries = statsCollector.TotalSkippedEntries
};

_statisticsSubject.OnNext(stats);
Expand All @@ -81,6 +80,8 @@ private void ProcessInventoryFile(InventoryFile inventoryFile, StatisticsCollect

foreach (var part in inventory.InventoryParts)
{
collector.TotalSkippedEntries += part.SkippedCount;

foreach (var dir in part.DirectoryDescriptions)
{
if (!dir.IsAccessible)
Expand Down Expand Up @@ -133,7 +134,7 @@ private static bool HasValidFingerprint(FileDescription fd)
return !string.IsNullOrEmpty(fd.Sha256) || !string.IsNullOrEmpty(fd.SignatureGuid);
}

private class StatisticsCollector
private sealed class StatisticsCollector
{
public int TotalAnalyzed { get; set; }

Expand All @@ -142,6 +143,8 @@ private class StatisticsCollector
public int Errors { get; set; }

public int IdentificationErrors { get; set; }

public int TotalSkippedEntries { get; set; }

public long ProcessedSize { get; set; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@

public class InventoryGlobalStatusViewModel : ActivatableViewModelBase
{
private const string MainSecondaryColorBrushKey = "MainSecondaryColor";
private const string HomeCloudSynchronizationBackGroundBrushKey = "HomeCloudSynchronizationBackGround";

private readonly IInventoryService _inventoryService = null!;
private readonly ISessionService _sessionService = null!;
private readonly IDialogService _dialogService = null!;
Expand Down Expand Up @@ -79,10 +82,15 @@

[Reactive]
public int? GlobalIdentificationErrors { get; set; }

[Reactive]
public int? GlobalSkippedEntries { get; set; }

public extern bool HasErrors { [ObservableAsProperty] get; }

public extern bool HasIdentificationErrors { [ObservableAsProperty] get; }

public extern bool HasGlobalSkippedEntries { [ObservableAsProperty] get; }

[Reactive]
public string GlobalMainIcon { get; set; } = "None";
Expand Down Expand Up @@ -112,6 +120,11 @@
.Select(e => (e ?? 0) > 0)
.ToPropertyEx(this, x => x.HasIdentificationErrors)
.DisposeWith(disposables);

this.WhenAnyValue(x => x.GlobalSkippedEntries)
.Select(e => (e ?? 0) > 0)
.ToPropertyEx(this, x => x.HasGlobalSkippedEntries)
.DisposeWith(disposables);
}

private ReactiveStreams CreateStreams(IInventoryStatisticsService inventoryStatisticsService, CompositeDisposable disposables)
Expand Down Expand Up @@ -200,7 +213,7 @@
.DisposeWith(disposables);
}

private IObservable<(string Icon, string Text, string BrushKey)> CreateNonSuccessVisual(
private static IObservable<(string Icon, string Text, string BrushKey)> CreateNonSuccessVisual(
IObservable<InventoryTaskStatus> statusStream)
{
return statusStream
Expand All @@ -210,10 +223,10 @@
InventoryTaskStatus.Cancelled => Resources.InventoryProcess_InventoryCancelled,
InventoryTaskStatus.Error => Resources.InventoryProcess_InventoryError,
_ => Resources.InventoryProcess_InventoryError
}, BrushKey: "MainSecondaryColor"));
}, BrushKey: MainSecondaryColorBrushKey));
}

private IObservable<(string Icon, string Text, string BrushKey)> CreateSuccessVisual(ReactiveStreams streams)

Check warning on line 229 in src/ByteSync.Client/ViewModels/Sessions/Inventories/InventoryGlobalStatusViewModel.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make 'CreateSuccessVisual' a static method.

See more on https://sonarcloud.io/project/issues?id=POW-Software_ByteSync&issues=AZ1IN2-DbDscp0XQ17Y_&open=AZ1IN2-DbDscp0XQ17Y_&pullRequest=280

Check warning on line 229 in src/ByteSync.Client/ViewModels/Sessions/Inventories/InventoryGlobalStatusViewModel.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'CreateSuccessVisual' does not access instance data and can be marked as static

See more on https://sonarcloud.io/project/issues?id=POW-Software_ByteSync&issues=AZ1IN2-DbDscp0XQ17ZB&open=AZ1IN2-DbDscp0XQ17ZB&pullRequest=280
{
return streams.StatusStream
.Select(st => st == InventoryTaskStatus.Success
Expand All @@ -222,7 +235,7 @@
.Switch();
}

private IObservable<(string Icon, string Text, string BrushKey)> CreateSuccessOnStatsVisual(ReactiveStreams streams)

Check warning on line 238 in src/ByteSync.Client/ViewModels/Sessions/Inventories/InventoryGlobalStatusViewModel.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make 'CreateSuccessOnStatsVisual' a static method.

See more on https://sonarcloud.io/project/issues?id=POW-Software_ByteSync&issues=AZ1IN2-DbDscp0XQ17ZA&open=AZ1IN2-DbDscp0XQ17ZA&pullRequest=280

Check warning on line 238 in src/ByteSync.Client/ViewModels/Sessions/Inventories/InventoryGlobalStatusViewModel.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'CreateSuccessOnStatsVisual' does not access instance data and can be marked as static

See more on https://sonarcloud.io/project/issues?id=POW-Software_ByteSync&issues=AZ1IN2-DbDscp0XQ17ZC&open=AZ1IN2-DbDscp0XQ17ZC&pullRequest=280
{
return streams.StatsStream
.Where(s => s != null)
Expand All @@ -231,18 +244,18 @@
.Select(t => GetSuccessVisualState(t.s.AnalyzeErrors));
}

private (string Icon, string Text, string BrushKey) GetSuccessVisualState(int? errors)
private static (string Icon, string Text, string BrushKey) GetSuccessVisualState(int? errors)
{
if (errors is > 0)
{
var text = Resources.ResourceManager.GetString("InventoryProcess_InventorySuccessWithErrors", Resources.Culture)
?? Resources.InventoryProcess_InventorySuccess;

return (Icon: "RegularError", Text: text, BrushKey: "MainSecondaryColor");
return (Icon: "RegularError", Text: text, BrushKey: MainSecondaryColorBrushKey);
}

return (Icon: "SolidCheckCircle", Text: Resources.InventoryProcess_InventorySuccess,
BrushKey: "HomeCloudSynchronizationBackGround");
BrushKey: HomeCloudSynchronizationBackGroundBrushKey);
}

private void SetupStatisticsSubscription(IInventoryStatisticsService inventoryStatisticsService,
Expand Down Expand Up @@ -270,6 +283,7 @@
GlobalAnalyzeSuccess = stats?.AnalyzeSuccess;
GlobalAnalyzeErrors = stats?.AnalyzeErrors;
GlobalIdentificationErrors = stats?.IdentificationErrors;
GlobalSkippedEntries = stats?.TotalSkippedEntries;
}

private void ApplySuccessState(int? errors, int? identificationErrors = null)
Expand All @@ -282,13 +296,13 @@
?? Resources.InventoryProcess_InventorySuccess;
GlobalMainIcon = "RegularError";
GlobalMainStatusText = text;
GlobalMainIconBrush = _themeService.GetBrush("MainSecondaryColor");
GlobalMainIconBrush = _themeService.GetBrush(MainSecondaryColorBrushKey);
}
else
{
GlobalMainIcon = "SolidCheckCircle";
GlobalMainStatusText = Resources.InventoryProcess_InventorySuccess;
GlobalMainIconBrush = _themeService.GetBrush("HomeCloudSynchronizationBackGround");
GlobalMainIconBrush = _themeService.GetBrush(HomeCloudSynchronizationBackGroundBrushKey);
}
}

Expand Down Expand Up @@ -332,6 +346,7 @@
GlobalAnalyzeSuccess = null;
GlobalAnalyzeErrors = null;
GlobalIdentificationErrors = null;
GlobalSkippedEntries = null;
GlobalMainIcon = "None";
GlobalMainStatusText = string.Empty;
GlobalMainIconBrush = null;
Expand Down Expand Up @@ -363,21 +378,19 @@
case InventoryTaskStatus.Error:
case InventoryTaskStatus.Cancelled:
case InventoryTaskStatus.NotLaunched:
GlobalMainIconBrush = _themeService.GetBrush("MainSecondaryColor");
GlobalMainIconBrush = _themeService.GetBrush(MainSecondaryColorBrushKey);

break;
case InventoryTaskStatus.Success:
var errors = GlobalAnalyzeErrors ?? 0;
var identificationErrors = GlobalIdentificationErrors ?? 0;
GlobalMainIconBrush = (errors + identificationErrors) > 0
? _themeService.GetBrush("MainSecondaryColor")
: _themeService.GetBrush("HomeCloudSynchronizationBackGround");
? _themeService.GetBrush(MainSecondaryColorBrushKey)
: _themeService.GetBrush(HomeCloudSynchronizationBackGroundBrushKey);

break;
case InventoryTaskStatus.Pending:
case InventoryTaskStatus.Running:
default:
GlobalMainIconBrush = _themeService.GetBrush("HomeCloudSynchronizationBackGround");
GlobalMainIconBrush = _themeService.GetBrush(HomeCloudSynchronizationBackGroundBrushKey);

break;
}
Expand All @@ -398,7 +411,7 @@
}
}

private record ReactiveStreams(
private sealed record ReactiveStreams(
IObservable<InventoryTaskStatus> StatusStream,
IObservable<SessionStatus> SessionPreparation,
IObservable<InventoryStatistics?> StatsStream,
Expand Down
Loading
Loading