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
2 changes: 1 addition & 1 deletion src/LageBuch.App/AppPaths.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace LageBuch.App;

public static class AppPaths
internal static class AppPaths
{
public static string AppDataDir =>
GetAppDataDir(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData));
Expand Down
4 changes: 4 additions & 0 deletions src/LageBuch.App/LageBuch.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@
<ProjectReference Include="..\LageBuch.Sync.Hosting\LageBuch.Sync.Hosting.csproj" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="LageBuch.App.Tests" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion src/LageBuch.App/Services/IncidentHostController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace LageBuch.App.Services;
/// reachable over loopback, the LAN, and a tailnet at once. Lives in the desktop head so ASP.NET
/// Core stays out of the cross-platform AppLogic/Android build.
/// </summary>
public sealed class IncidentHostController : IIncidentHostController
internal sealed class IncidentHostController : IIncidentHostController
{
private readonly IClock _clock;
private readonly string _appVersion;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace LageBuch.App.Services;

public sealed class StorageProviderFileDialogService : IFileDialogService
internal sealed class StorageProviderFileDialogService : IFileDialogService
{
private static readonly FilePickerFileType Incident =
new("Einsatzdokumentation") { Patterns = new[] { "*.fwincident" } };
Expand Down
2 changes: 1 addition & 1 deletion src/LageBuch.App/Services/SystemAlarmService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace LageBuch.App.Services;
/// Every path degrades to a silent no-op when the asset or the player is missing, so a build with
/// no voice clip yet — or a host without the CLI player — simply stays quiet rather than crashing.
/// </summary>
public sealed class SystemAlarmService : IAlarmService
internal sealed class SystemAlarmService : IAlarmService
{
private const uint SndAsync = 0x0001; // play asynchronously
private const uint SndNodefault = 0x0002; // no default beep if it fails
Expand Down
7 changes: 3 additions & 4 deletions src/LageBuch.AppLogic/Services/IncidentStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ namespace LageBuch.AppLogic.Services;

public sealed class IncidentStore : IIncidentStore
{
private readonly IncidentRepository _repository = new();
private readonly IIncidentFileStore _fileStore = new IncidentFileStore();

public void Save(string path, Incident incident) => _repository.Save(path, incident);
public void Save(string path, Incident incident) => IncidentRepository.Save(path, incident);

public Incident Load(string path) => _repository.Load(path);
public Incident Load(string path) => IncidentRepository.Load(path);

public IncidentState? TryReadState(string path) => _repository.TryReadState(path);
public IncidentState? TryReadState(string path) => IncidentRepository.TryReadState(path);

public void SaveFileBytes(string path, string storageFileName, byte[] bytes) =>
_fileStore.SaveBytes(path, storageFileName, bytes);
Expand Down
7 changes: 3 additions & 4 deletions src/LageBuch.AppLogic/Services/MasterDataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,17 @@ namespace LageBuch.AppLogic.Services;
public sealed class MasterDataProvider : IMasterDataProvider
{
private readonly string _path;
private readonly MasterDataStore _store = new();
private MasterDataSet? _cached;

public MasterDataProvider(string masterDataPath) => _path = masterDataPath;

public MasterDataSet Get() => _cached ??= _store.GetOrCreate(_path);
public MasterDataSet Get() => _cached ??= MasterDataStore.GetOrCreate(_path);

public void Save(MasterDataSet set)
{
_store.Save(_path, set);
MasterDataStore.Save(_path, set);
// Re-read rather than trust the in-memory copy: the store is the canonical shape
// (e.g. personnel comes back name-sorted), so callers see exactly what a fresh start would.
_cached = _store.GetOrCreate(_path);
_cached = MasterDataStore.GetOrCreate(_path);
}
}
5 changes: 5 additions & 0 deletions src/LageBuch.AppLogic/ViewModels/AboutViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,18 @@ public AboutViewModel(IFileDialogService dialogs, string version)
Version = version;
}

[SuppressMessage("Performance", "CA1822", Justification = "XAML {Binding} target in AboutView; binding requires an instance property.")]
public string AppName => "Lagebuch";
[SuppressMessage("Performance", "CA1822", Justification = "XAML {Binding} target in AboutView; binding requires an instance property.")]
public string Descriptor => "Einsatzdokumentation";
public string Version { get; }
[SuppressMessage("Performance", "CA1822", Justification = "XAML {Binding} target in AboutView; binding requires an instance property.")]
public string RepositoryUrl => RepoUrl;

// Kept in sync with the LICENSE file in the repo root.
[SuppressMessage("Performance", "CA1822", Justification = "XAML {Binding} target in AboutView; binding requires an instance property.")]
public string LicenseLine => "Veröffentlicht unter der MIT-Lizenz.";
[SuppressMessage("Performance", "CA1822", Justification = "XAML {Binding} target in AboutView; binding requires an instance property.")]
public string CopyrightLine => "Copyright © 2026 Thomas Müller";

[ObservableProperty]
Expand Down
8 changes: 4 additions & 4 deletions src/LageBuch.Persistence/IncidentRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public sealed class IncidentRepository
{
private const string Iso = "O";

public void Save(string path, Incident incident)
public static void Save(string path, Incident incident)
{
ArgumentNullException.ThrowIfNull(incident);
using var cn = SqliteConnectionFactory.OpenReadWrite(path);
Expand Down Expand Up @@ -269,9 +269,9 @@ private static void WriteChecklist(SqliteConnection cn, SqliteTransaction tx, IR
/// `state` lives in the base schema's <c>incident_meta</c>, so this works across schema versions;
/// any failure (missing, corrupt, locked, too new) returns null so the overview degrades quietly.
/// </summary>
[SuppressMessage("Design", "CA1031",
[SuppressMessage("Design", "CA1031",
Justification = "Try-read: missing, corrupt, locked or too-new reads all degrade to null (see comment).")]
public IncidentState? TryReadState(string path)
public static IncidentState? TryReadState(string path)
{
if (!File.Exists(path))
return null;
Expand All @@ -289,7 +289,7 @@ private static void WriteChecklist(SqliteConnection cn, SqliteTransaction tx, IR
}
}

public Incident Load(string path)
public static Incident Load(string path)
{
// Check before opening: SQLite would otherwise report a missing file as a bare "unable to
// open database file", and the caller cannot tell that apart from a corrupt one.
Expand Down
4 changes: 2 additions & 2 deletions src/LageBuch.Persistence/MasterData/MasterDataStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public sealed class MasterDataStore
/// its contents. Nothing is seeded: the app ships with no master data, so a fresh database comes
/// back empty and is populated only by <see cref="Save"/> — i.e. the editor's Import.
/// </summary>
public MasterDataSet GetOrCreate(string path)
public static MasterDataSet GetOrCreate(string path)
{
using var cn = SqliteConnectionFactory.OpenReadWrite(path);
EnsureSchema(cn);
Expand All @@ -22,7 +22,7 @@ public MasterDataSet GetOrCreate(string path)
/// Replaces the master data with <paramref name="set"/>, in the given order. A full transactional
/// replace, so deletes and reorders take effect exactly as supplied.
/// </summary>
public void Save(string path, MasterDataSet set)
public static void Save(string path, MasterDataSet set)
{
ArgumentNullException.ThrowIfNull(set);
using var cn = SqliteConnectionFactory.OpenReadWrite(path);
Expand Down
2 changes: 1 addition & 1 deletion tests/LageBuch.Acceptance.Tests/TestAppBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace LageBuch.Acceptance.Tests;

public static class TestAppBuilder
internal static class TestAppBuilder
{
// UseHeadlessDrawing = false keeps the real Skia text/render backend (bundled via
// Avalonia.Desktop) so embedded custom fonts can be rasterized. The default headless
Expand Down
2 changes: 1 addition & 1 deletion tests/LageBuch.Documents.Tests/PdfAssert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace LageBuch.Documents.Tests;

public static class PdfAssert
internal static class PdfAssert
{
private static readonly byte[] PdfHeader = { 0x25, 0x50, 0x44, 0x46, 0x2D }; // "%PDF-"

Expand Down
2 changes: 1 addition & 1 deletion tests/LageBuch.Domain.Tests/IncidentCreationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace LageBuch.Domain.Tests;

public sealed class FixedClock : IClock
internal sealed class FixedClock : IClock
{
public FixedClock(DateTimeOffset now) => Now = now;
public DateTimeOffset Now { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ private void SaveClosedIncident()
var op = new SessionOperator("Müller");
var incident = Incident.Start(clock, op);
incident.Close(clock, op);
new IncidentRepository().Save(_path, incident);
IncidentRepository.Save(_path, incident);
}

[Fact]
Expand All @@ -39,7 +39,7 @@ public void ReadOnly_connection_rejects_writes()
public void Load_of_closed_incident_returns_readonly_domain_state()
{
SaveClosedIncident();
var loaded = new IncidentRepository().Load(_path);
var loaded = IncidentRepository.Load(_path);
Assert.Equal(IncidentState.Closed, loaded.State);
Assert.Throws<IncidentClosedException>(
() => loaded.AddForceUnit(new Clock(), new Domain.SessionOperator("Müller"), "FFB", 1));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ private static Incident CreateIncidentWithBuilding()
public void SaveLoad_RoundTrip_BuildingsAndDwellings()
{
var original = CreateIncidentWithBuilding();
var repo = new IncidentRepository();
repo.Save(_path, original);

var loaded = repo.Load(_path);
IncidentRepository.Save(_path, original);

var loaded = IncidentRepository.Load(_path);

Assert.Single(loaded.Buildings);
Assert.Equal("Haus A", loaded.Buildings[0].Name);
Expand Down Expand Up @@ -69,10 +69,9 @@ public void SaveLoad_RoundTrip_NullableFields()
var incident = Incident.Start(clock, op);
incident.AddCoBuilding(clock, op, "Haus A", 1, 1);

var repo = new IncidentRepository();
repo.Save(_path, incident);
IncidentRepository.Save(_path, incident);

var loaded = repo.Load(_path);
var loaded = IncidentRepository.Load(_path);

var dwelling = loaded.Dwellings[0];
Assert.Null(dwelling.CoValue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public void Loading_a_missing_file_reports_it_and_creates_nothing()
{
Assert.False(File.Exists(_path));

var ex = Assert.Throws<FileNotFoundException>(() => new IncidentRepository().Load(_path));
var ex = Assert.Throws<FileNotFoundException>(() => IncidentRepository.Load(_path));

// The path rides on FileName, not in the message: the Home banner already prefixes the
// filename, so repeating it there reads as a stutter.
Expand All @@ -35,7 +35,7 @@ public void Loading_a_file_that_is_not_a_database_reports_it_without_truncating_
// Picking the wrong file in the open dialog must not damage whatever was picked.
File.WriteAllText(_path, "nicht wirklich eine Einsatzdatei");

Assert.ThrowsAny<Exception>(() => new IncidentRepository().Load(_path));
Assert.ThrowsAny<Exception>(() => IncidentRepository.Load(_path));

Assert.Equal("nicht wirklich eine Einsatzdatei", File.ReadAllText(_path));

Expand Down
16 changes: 8 additions & 8 deletions tests/LageBuch.Persistence.Tests/IncidentRepositorySaveTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public void Save_writes_meta_and_journal_rows()
incident.SetIncidentNumber(new IncidentNumber("B 1.2 260715 4242"));
incident.AddJournalEntry(clock, op, EtbDirection.Incoming, "Meldung", from: "ILS");

new IncidentRepository().Save(_path, incident);
IncidentRepository.Save(_path, incident);

using var cn = SqliteConnectionFactory.OpenReadOnly(_path);
using var cmd = cn.CreateCommand();
Expand All @@ -49,7 +49,7 @@ public void Save_writes_one_etb_entry_edits_row_per_edit()
incident.EditJournalEntry(clock, op, entry.Id, "Erste Korrektur");
incident.EditJournalEntry(clock, op, entry.Id, "Zweite Korrektur");

new IncidentRepository().Save(_path, incident);
IncidentRepository.Save(_path, incident);

using var cn = SqliteConnectionFactory.OpenReadOnly(_path);
using var cmd = cn.CreateCommand();
Expand All @@ -62,8 +62,8 @@ public void Save_writes_one_etb_entry_edits_row_per_edit()
public void Legacy_ils_number_loads_as_the_incident_number()
{
var clock = new Clock();
var repo = new IncidentRepository();
repo.Save(_path, Incident.Start(clock, new SessionOperator("Müller")));

IncidentRepository.Save(_path, Incident.Start(clock, new SessionOperator("Müller")));

// Simulate a file written before the unification: the number lived in ils_number and
// incident_number was empty.
Expand All @@ -75,7 +75,7 @@ public void Legacy_ils_number_loads_as_the_incident_number()
}
SqliteConnection.ClearAllPools();

Assert.Equal("4711", repo.Load(_path).IncidentNumber!.Value);
Assert.Equal("4711", IncidentRepository.Load(_path).IncidentNumber!.Value);
}

[Fact]
Expand All @@ -84,9 +84,9 @@ public void Save_is_idempotent_overwrite_not_append()
var clock = new Clock();
var op = new SessionOperator("Müller");
var incident = Incident.Start(clock, op);
var repo = new IncidentRepository();
repo.Save(_path, incident);
repo.Save(_path, incident);

IncidentRepository.Save(_path, incident);
IncidentRepository.Save(_path, incident);

using var cn = SqliteConnectionFactory.OpenReadOnly(_path);
using var cmd = cn.CreateCommand();
Expand Down
Loading
Loading