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
5 changes: 3 additions & 2 deletions docs/features/F1.41-persistent-history.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Status: in progress for 0.8.0 ([#314](https://github.com/APRS-NET/AprsMap/issues/314), maintainer request, 2026-10-27);
decisions 1–6 by the maintainer, the rest proposed. Built: the store, rotation, the size warning in Settings, the
settings and the configuration (behaviors 1–6, 12, 13), the restart (7), long tracks and kept packets (8, 9), the
dashboard's 7 days (10, 11). To come: the Connection tile's note (5).
dashboard's 7 days (10, 11), the Connection tile's note (5).

## User story

Expand Down Expand Up @@ -39,7 +39,8 @@ and how much disk that may take, and I'm told when the disk limit, not the days,
own limits the same way: **Keep packet history** on or off, **Keep for** 1–30 days, default **7**; **Use at most**
100–102,400 MB, default **1,024 MB** (decision 2). The gateway's configuration can set both (`[engine.history]
days = 7`, `max_megabytes = 1024`, `days = 0` for off), and wins on every start, as the configured devices do
(#424); Settings then shows them without letting them be changed. Off keeps today's behaviour: memory only, and the history kept so far is deleted after a confirmation.
(#424); Settings then shows them without letting them be changed. The file's values are brought into range, as
Settings would refuse them. Off keeps today's behaviour: memory only, and the history kept so far is deleted after a confirmation.
3. **The section says what's kept**: "7 days kept, 412 MB, since 21/10/2026 14:02", and how full the size limit is.
4. **One file a day** (UTC), `history/2026-10-27.db` in the data folder, so rotating is deleting whole files: no
rewriting, no pause. Once a minute (with the station sweep) and when the settings change, the engine deletes the days older than **Keep history for**, then
Expand Down
8 changes: 7 additions & 1 deletion src/AprsMap.Contracts/Dashboard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ public enum DashboardRange
/// The one source these numbers are for (#454); null for every source. The tiles read from the stations held (farthest
/// heard, weather, the tables, heard on RF) are every source's either way.
/// </param>
/// <param name="History">
/// What stands in the packet history's way, for the Connection tile (docs/features/F1.41-persistent-history.md, behavior
/// 5); null when nothing does or no history is kept. Only the size limit's problems, with the days asked for in
/// <see cref="HistoryStatus.Settings"/> and those that fit in <see cref="HistoryStatus.DaysFit"/>.
/// </param>
/// <param name="Comparison">
/// The sources side by side, for every source's dashboard with two or more (#454); over 7 days, the last 24 hours.
/// </param>
Expand Down Expand Up @@ -107,7 +112,8 @@ public sealed record Dashboard(
int HeardOnRfTotal = 0,
TimeSpan? ListsCover = null,
string? SourceId = null,
IReadOnlyList<DashboardSourceRow>? Comparison = null
IReadOnlyList<DashboardSourceRow>? Comparison = null,
HistoryStatus? History = null
);

/// <summary>One source against the others over the range (#454).</summary>
Expand Down
1 change: 1 addition & 0 deletions src/AprsMap.Core/Engine.DashboardSources.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ dashboard with
Weather = _stations.WeatherSince(dashboard.Start, include),
Tables = _stations.Statistics(include is null ? null : filter, now),
Gps = _gpsHistory.Read(dashboard.Start, dashboard.Step, dashboard.Packets.Count, now),
History = _history?.SizeProblem(), // F1.41 behavior 5
}
);
}
Expand Down
1 change: 1 addition & 0 deletions src/AprsMap.Core/Engine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2081,6 +2081,7 @@ public async Task<Dashboard> GetDashboardAsync(
HeardOnRf = heard.Recent, // F3.4 two-way, behavior 7
HeardOnRfTotal = heard.Total,
Comparison = CompareSources(range, now), // #454
History = _history?.SizeProblem(), // F1.41 behavior 5
};
}

Expand Down
14 changes: 12 additions & 2 deletions src/AprsMap.Core/EngineOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,19 @@ public sealed class HistoryOptions
/// <summary>Whether the configuration sets either.</summary>
internal bool Configured => Days is not null || MaxMegabytes is not null;

/// <summary><paramref name="stored"/> with what the configuration sets in place.</summary>
/// <summary>
/// <paramref name="stored"/> with what the configuration sets in place, brought into range: the file isn't checked
/// as Settings is.
/// </summary>
internal HistorySettings Apply(HistorySettings stored) =>
new(Days ?? stored.Days, MaxMegabytes ?? stored.MaxMegabytes);
new(
Math.Clamp(Days ?? stored.Days, 0, HistorySettings.MaxDays),
Math.Clamp(
MaxMegabytes ?? stored.MaxMegabytes,
HistorySettings.MinMegabytes,
HistorySettings.MaxMegabytesLimit
)
);
}

/// <summary>A first-run gpsd (docs/features/F1.37-gateway-service.md, behavior 4).</summary>
Expand Down
19 changes: 19 additions & 0 deletions src/AprsMap.Core/PacketHistory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,25 @@ public async Task MaintainAsync(CancellationToken cancellationToken = default)
}
}

/// <summary>
/// The size limit's problem as the last pass found it, without reading the folder: for the dashboard, asked every
/// 10 seconds (behavior 5). Null when there's none.
/// </summary>
public HistoryStatus? SizeProblem()
{
lock (_lock)
{
var problem =
!_settings.On || _blocked != HistoryProblem.None ? HistoryProblem.None
: _fullDay == Day(_time.GetUtcNow()) ? HistoryProblem.TodayFull
: _daysFit is not null ? HistoryProblem.SizeLimitShortens
: HistoryProblem.None;
return problem == HistoryProblem.None
? null
: new HistoryStatus(true, _settings, Problem: problem, DaysFit: _daysFit);
}
}

/// <summary>What the history holds now (behavior 3).</summary>
public async Task<HistoryStatus> StatusAsync(bool configured, CancellationToken cancellationToken = default)
{
Expand Down
8 changes: 8 additions & 0 deletions src/AprsMap.UI/Statistics/DashboardPanel.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,14 @@
<TextBlock Classes="title" Text="{l:T Dashboard.Connection}" />
<ItemsControl ItemsSource="{Binding Connections}" Margin="0,4,0,0" />
<TextBlock Classes="detail" Text="{Binding ReconnectsText}" Margin="0,4,0,0" />
<TextBlock
x:Name="HistoryWarning"
Text="{Binding HistoryWarning}"
IsVisible="{Binding HistoryWarning, Converter={x:Static ObjectConverters.IsNotNull}}"
Foreground="{DynamicResource Brush.Warn}"
TextWrapping="Wrap"
Margin="0,4,0,0"
/>
</StackPanel>
</Border>

Expand Down
16 changes: 16 additions & 0 deletions src/AprsMap.UI/Statistics/DashboardViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,13 @@ private void ShowScope()
[ObservableProperty]
public partial string? ReconnectsText { get; private set; }

/// <summary>
/// That the packet history's size limit is cutting it short, in the Connection tile
/// (docs/features/F1.41-persistent-history.md, behavior 5); null otherwise.
/// </summary>
[ObservableProperty]
public partial string? HistoryWarning { get; private set; }

/// <summary>The busiest stations (behavior 4.8), each selectable.</summary>
public ObservableCollection<CountRow> BusiestStations { get; } = [];

Expand Down Expand Up @@ -457,6 +464,15 @@ [.. rf.Select((count, i) => dashboard.Packets[i] == 0 ? 0 : 100 * count / dashbo
ReconnectsText = status is null
? null
: _text.Format("Dashboard.Reconnects", new Dictionary<string, object?> { ["count"] = status.Reconnects });
HistoryWarning = dashboard.History switch
{
{ Problem: HistoryProblem.TodayFull } => _text["History.TodayFull"],
{ Problem: HistoryProblem.SizeLimitShortens } history => _text.Format(
"History.SizeLimitShortens",
new Dictionary<string, object?> { ["fit"] = history.DaysFit ?? 0, ["wanted"] = history.Settings.Days }
),
_ => null,
};

ShowExtras(dashboard);
ShowGps(dashboard, range, Clock);
Expand Down
11 changes: 11 additions & 0 deletions tests/AprsMap.Core.Tests/PacketHistoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ public async Task WhenTodayAloneReachesTheLimitNothingMoreIsKeptToday()

Assert.Equal(before, (await ReadAll(history)).Count);
Assert.Equal(HistoryProblem.TodayFull, (await history.StatusAsync(false, Ct)).Problem);
Assert.Equal(HistoryProblem.TodayFull, history.SizeProblem()!.Problem); // for the dashboard (behavior 5)
_time.Advance(TimeSpan.FromDays(1)); // a new day starts again
history.Record(Record());
await history.FlushAsync(Ct);
Expand Down Expand Up @@ -226,6 +227,16 @@ public async Task TheConfigurationsLengthAndSizeWinAndAreShownAsConfigured()
Assert.Equal((new HistorySettings(3, 500), true), (status.Settings, status.Configured));
}

[Fact]
public async Task TheConfigurationsValuesAreBroughtIntoRange()
{
var options = new HistoryOptions { Days = 90, MaxMegabytes = 1 };
await using var history = Store();

Assert.Equal(new HistorySettings(30, 100), options.Apply(HistorySettings.Default));
Assert.Null(history.SizeProblem()); // nothing standing in the way
}

[Fact]
public async Task OutOfRangeSettingsAreRefused()
{
Expand Down
25 changes: 25 additions & 0 deletions tests/AprsMap.UI.Tests/DashboardViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,31 @@ public DashboardViewModel Create() =>
);
}

[Fact]
public void TheConnectionTileSaysWhenTheHistorysSizeLimitCutsItShort()
{
// F1.41 behavior 5.
var vm = new Harness().Create();

vm.Show(
Sample() with
{
History = new HistoryStatus(
true,
HistorySettings.Default,
Problem: HistoryProblem.SizeLimitShortens,
DaysFit: 3
),
}
);
Assert.Equal(
"The size limit keeps about 3 days of the 7 asked for. Raise it to keep them all.",
vm.HistoryWarning
);
vm.Show(Sample());
Assert.Null(vm.HistoryWarning);
}

[Fact]
public async Task WithTwoSourcesOneCanBeShownAndTheyAreCompared()
{
Expand Down
Loading