From 10681f194990c9ac905138d239aef80179f22140 Mon Sep 17 00:00:00 2001
From: luckydizzier <134082331+luckydizzier@users.noreply.github.com>
Date: Wed, 1 Oct 2025 21:57:14 +0200
Subject: [PATCH] feat(demo): scaffold Avalonia demo shell
---
src/demo/XBase.Demo.App/App.axaml | 5 +
src/demo/XBase.Demo.App/App.axaml.cs | 29 +++++
.../ServiceCollectionExtensions.cs | 24 ++++
src/demo/XBase.Demo.App/Program.cs | 44 +++++++
.../ViewModels/ShellViewModel.cs | 107 ++++++++++++++++++
.../XBase.Demo.App/Views/MainWindow.axaml | 22 ++++
.../XBase.Demo.App/Views/MainWindow.axaml.cs | 32 ++++++
src/demo/XBase.Demo.App/XBase.Demo.App.csproj | 20 ++++
.../InMemoryTelemetrySink.cs | 39 +++++++
.../ServiceCollectionExtensions.cs | 21 ++++
.../XBase.Demo.Diagnostics.csproj | 11 ++
.../XBase.Demo.Domain/Catalog/CatalogModel.cs | 26 +++++
.../XBase.Demo.Domain/Catalog/TablePage.cs | 21 ++++
.../Diagnostics/DemoTelemetryEvent.cs | 20 ++++
.../Services/ITableCatalogService.cs | 19 ++++
.../Services/ITablePageService.cs | 13 +++
.../XBase.Demo.Domain.csproj | 6 +
.../Catalog/FileSystemTableCatalogService.cs | 61 ++++++++++
.../Catalog/NullTablePageService.cs | 21 ++++
.../ServiceCollectionExtensions.cs | 27 +++++
.../XBase.Demo.Infrastructure.csproj | 14 +++
src/demo/tasks.md | 25 ++++
xBase.sln | 31 +++++
23 files changed, 638 insertions(+)
create mode 100644 src/demo/XBase.Demo.App/App.axaml
create mode 100644 src/demo/XBase.Demo.App/App.axaml.cs
create mode 100644 src/demo/XBase.Demo.App/DependencyInjection/ServiceCollectionExtensions.cs
create mode 100644 src/demo/XBase.Demo.App/Program.cs
create mode 100644 src/demo/XBase.Demo.App/ViewModels/ShellViewModel.cs
create mode 100644 src/demo/XBase.Demo.App/Views/MainWindow.axaml
create mode 100644 src/demo/XBase.Demo.App/Views/MainWindow.axaml.cs
create mode 100644 src/demo/XBase.Demo.App/XBase.Demo.App.csproj
create mode 100644 src/demo/XBase.Demo.Diagnostics/InMemoryTelemetrySink.cs
create mode 100644 src/demo/XBase.Demo.Diagnostics/ServiceCollectionExtensions.cs
create mode 100644 src/demo/XBase.Demo.Diagnostics/XBase.Demo.Diagnostics.csproj
create mode 100644 src/demo/XBase.Demo.Domain/Catalog/CatalogModel.cs
create mode 100644 src/demo/XBase.Demo.Domain/Catalog/TablePage.cs
create mode 100644 src/demo/XBase.Demo.Domain/Diagnostics/DemoTelemetryEvent.cs
create mode 100644 src/demo/XBase.Demo.Domain/Services/ITableCatalogService.cs
create mode 100644 src/demo/XBase.Demo.Domain/Services/ITablePageService.cs
create mode 100644 src/demo/XBase.Demo.Domain/XBase.Demo.Domain.csproj
create mode 100644 src/demo/XBase.Demo.Infrastructure/Catalog/FileSystemTableCatalogService.cs
create mode 100644 src/demo/XBase.Demo.Infrastructure/Catalog/NullTablePageService.cs
create mode 100644 src/demo/XBase.Demo.Infrastructure/ServiceCollectionExtensions.cs
create mode 100644 src/demo/XBase.Demo.Infrastructure/XBase.Demo.Infrastructure.csproj
create mode 100644 src/demo/tasks.md
diff --git a/src/demo/XBase.Demo.App/App.axaml b/src/demo/XBase.Demo.App/App.axaml
new file mode 100644
index 0000000..ed4fd08
--- /dev/null
+++ b/src/demo/XBase.Demo.App/App.axaml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/src/demo/XBase.Demo.App/App.axaml.cs b/src/demo/XBase.Demo.App/App.axaml.cs
new file mode 100644
index 0000000..edc89e8
--- /dev/null
+++ b/src/demo/XBase.Demo.App/App.axaml.cs
@@ -0,0 +1,29 @@
+using System;
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using Microsoft.Extensions.DependencyInjection;
+using XBase.Demo.App.Views;
+
+namespace XBase.Demo.App;
+
+public partial class App : Application
+{
+ public static IServiceProvider? Services { get; set; }
+
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ var services = Services ?? throw new InvalidOperationException("Service provider is not initialized.");
+ desktop.MainWindow = services.GetRequiredService();
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+}
diff --git a/src/demo/XBase.Demo.App/DependencyInjection/ServiceCollectionExtensions.cs b/src/demo/XBase.Demo.App/DependencyInjection/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..ea22e11
--- /dev/null
+++ b/src/demo/XBase.Demo.App/DependencyInjection/ServiceCollectionExtensions.cs
@@ -0,0 +1,24 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using XBase.Demo.App.ViewModels;
+using XBase.Demo.App.Views;
+using XBase.Demo.Diagnostics;
+using XBase.Demo.Infrastructure;
+
+namespace XBase.Demo.App.DependencyInjection;
+
+public static class ServiceCollectionExtensions
+{
+ public static IServiceCollection AddDemoApp(this IServiceCollection services)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+
+ services.AddXBaseDemoInfrastructure();
+ services.AddXBaseDemoDiagnostics();
+
+ services.AddSingleton();
+ services.AddSingleton();
+
+ return services;
+ }
+}
diff --git a/src/demo/XBase.Demo.App/Program.cs b/src/demo/XBase.Demo.App/Program.cs
new file mode 100644
index 0000000..0e0543e
--- /dev/null
+++ b/src/demo/XBase.Demo.App/Program.cs
@@ -0,0 +1,44 @@
+using System;
+using Avalonia;
+using Avalonia.ReactiveUI;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using XBase.Demo.App.DependencyInjection;
+
+namespace XBase.Demo.App;
+
+public static class Program
+{
+ [STAThread]
+ public static int Main(string[] args)
+ {
+ using var host = CreateHost(args);
+ host.Start();
+
+ App.Services = host.Services;
+
+ try
+ {
+ return BuildAvaloniaApp()
+ .AfterSetup(_ => App.Services = host.Services)
+ .StartWithClassicDesktopLifetime(args);
+ }
+ finally
+ {
+ host.StopAsync().GetAwaiter().GetResult();
+ }
+ }
+
+ private static IHost CreateHost(string[] args)
+ {
+ var builder = Host.CreateApplicationBuilder(args);
+ builder.Services.AddDemoApp();
+ return builder.Build();
+ }
+
+ private static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect()
+ .LogToTrace()
+ .UseReactiveUI();
+}
diff --git a/src/demo/XBase.Demo.App/ViewModels/ShellViewModel.cs b/src/demo/XBase.Demo.App/ViewModels/ShellViewModel.cs
new file mode 100644
index 0000000..d2df08e
--- /dev/null
+++ b/src/demo/XBase.Demo.App/ViewModels/ShellViewModel.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using ReactiveUI;
+using XBase.Demo.Domain.Catalog;
+using XBase.Demo.Domain.Diagnostics;
+using XBase.Demo.Domain.Services;
+
+namespace XBase.Demo.App.ViewModels;
+
+public class ShellViewModel : ReactiveObject
+{
+ private readonly ITableCatalogService _catalogService;
+ private readonly ITablePageService _pageService;
+ private readonly IDemoTelemetrySink _telemetrySink;
+ private readonly ILogger _logger;
+
+ private string? _catalogRoot;
+ private string? _tableSummary;
+ private bool _isBusy;
+
+ public ShellViewModel(
+ ITableCatalogService catalogService,
+ ITablePageService pageService,
+ IDemoTelemetrySink telemetrySink,
+ ILogger logger)
+ {
+ _catalogService = catalogService;
+ _pageService = pageService;
+ _telemetrySink = telemetrySink;
+ _logger = logger;
+
+ OpenCatalogCommand = ReactiveCommand.CreateFromTask(ExecuteOpenCatalogAsync);
+ OpenCatalogCommand.ThrownExceptions.Subscribe(OnOpenCatalogFault);
+ }
+
+ public ReactiveCommand OpenCatalogCommand { get; }
+
+ public string? CatalogRoot
+ {
+ get => _catalogRoot;
+ private set => this.RaiseAndSetIfChanged(ref _catalogRoot, value);
+ }
+
+ public string? TableSummary
+ {
+ get => _tableSummary;
+ private set => this.RaiseAndSetIfChanged(ref _tableSummary, value);
+ }
+
+ public bool IsBusy
+ {
+ get => _isBusy;
+ private set => this.RaiseAndSetIfChanged(ref _isBusy, value);
+ }
+
+ private async Task ExecuteOpenCatalogAsync(string rootPath)
+ {
+ if (string.IsNullOrWhiteSpace(rootPath))
+ {
+ return;
+ }
+
+ try
+ {
+ IsBusy = true;
+ CatalogRoot = rootPath;
+
+ var catalog = await _catalogService.LoadCatalogAsync(rootPath);
+ var payload = new Dictionary
+ {
+ ["root"] = catalog.RootPath,
+ ["tableCount"] = catalog.Tables.Count
+ };
+ _telemetrySink.Publish(new DemoTelemetryEvent("CatalogLoaded", DateTimeOffset.UtcNow, payload));
+
+ if (catalog.Tables.Count > 0)
+ {
+ var firstTable = catalog.Tables[0];
+ var page = await _pageService.LoadPageAsync(firstTable, new TablePageRequest(0, 25));
+ TableSummary = $"{catalog.Tables.Count} tables discovered. Preview rows: {page.Rows.Count} from {firstTable.Name}.";
+ }
+ else
+ {
+ TableSummary = "Catalog scanned successfully with no tables detected.";
+ }
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ private void OnOpenCatalogFault(Exception exception)
+ {
+ _logger.LogError(exception, "Catalog load failed");
+ var payload = new Dictionary
+ {
+ ["message"] = exception.Message
+ };
+ _telemetrySink.Publish(new DemoTelemetryEvent("CatalogLoadFailed", DateTimeOffset.UtcNow, payload));
+ TableSummary = "Catalog load failed. Review diagnostics for details.";
+ }
+}
diff --git a/src/demo/XBase.Demo.App/Views/MainWindow.axaml b/src/demo/XBase.Demo.App/Views/MainWindow.axaml
new file mode 100644
index 0000000..f0fce1d
--- /dev/null
+++ b/src/demo/XBase.Demo.App/Views/MainWindow.axaml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/demo/XBase.Demo.App/Views/MainWindow.axaml.cs b/src/demo/XBase.Demo.App/Views/MainWindow.axaml.cs
new file mode 100644
index 0000000..76766da
--- /dev/null
+++ b/src/demo/XBase.Demo.App/Views/MainWindow.axaml.cs
@@ -0,0 +1,32 @@
+using System;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using Microsoft.Extensions.DependencyInjection;
+using XBase.Demo.App.ViewModels;
+
+namespace XBase.Demo.App.Views;
+
+public partial class MainWindow : ReactiveWindow
+{
+ public MainWindow()
+ : this(ResolveViewModel())
+ {
+ }
+
+ public MainWindow(ShellViewModel viewModel)
+ {
+ InitializeComponent();
+ DataContext = viewModel;
+ }
+
+ private static ShellViewModel ResolveViewModel()
+ {
+ var services = App.Services ?? throw new InvalidOperationException("Service provider is not available for MainWindow.");
+ return services.GetRequiredService();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
diff --git a/src/demo/XBase.Demo.App/XBase.Demo.App.csproj b/src/demo/XBase.Demo.App/XBase.Demo.App.csproj
new file mode 100644
index 0000000..36a96a8
--- /dev/null
+++ b/src/demo/XBase.Demo.App/XBase.Demo.App.csproj
@@ -0,0 +1,20 @@
+
+
+ WinExe
+ XBase.Demo.App
+ XBase.Demo.App
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/demo/XBase.Demo.Diagnostics/InMemoryTelemetrySink.cs b/src/demo/XBase.Demo.Diagnostics/InMemoryTelemetrySink.cs
new file mode 100644
index 0000000..04737dc
--- /dev/null
+++ b/src/demo/XBase.Demo.Diagnostics/InMemoryTelemetrySink.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using Microsoft.Extensions.Logging;
+using XBase.Demo.Domain.Diagnostics;
+
+namespace XBase.Demo.Diagnostics;
+
+///
+/// Captures telemetry events in-memory for UI consumption.
+///
+public sealed class InMemoryTelemetrySink : IDemoTelemetrySink
+{
+ private readonly ConcurrentQueue _events = new();
+ private readonly ILogger _logger;
+
+ public InMemoryTelemetrySink(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public void Publish(DemoTelemetryEvent telemetryEvent)
+ {
+ ArgumentNullException.ThrowIfNull(telemetryEvent);
+
+ _events.Enqueue(telemetryEvent);
+ while (_events.Count > 128 && _events.TryDequeue(out _))
+ {
+ }
+
+ _logger.LogInformation("Telemetry event {Name} captured", telemetryEvent.Name);
+ }
+
+ ///
+ /// Returns a snapshot of the buffered events for visualization.
+ ///
+ public IReadOnlyCollection GetSnapshot()
+ => _events.ToArray();
+}
diff --git a/src/demo/XBase.Demo.Diagnostics/ServiceCollectionExtensions.cs b/src/demo/XBase.Demo.Diagnostics/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..d12f9aa
--- /dev/null
+++ b/src/demo/XBase.Demo.Diagnostics/ServiceCollectionExtensions.cs
@@ -0,0 +1,21 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using XBase.Demo.Domain.Diagnostics;
+
+namespace XBase.Demo.Diagnostics;
+
+///
+/// Registers diagnostics sinks for the demo experience.
+///
+public static class ServiceCollectionExtensions
+{
+ public static IServiceCollection AddXBaseDemoDiagnostics(this IServiceCollection services)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+
+ services.AddSingleton();
+ services.AddSingleton(sp => sp.GetRequiredService());
+
+ return services;
+ }
+}
diff --git a/src/demo/XBase.Demo.Diagnostics/XBase.Demo.Diagnostics.csproj b/src/demo/XBase.Demo.Diagnostics/XBase.Demo.Diagnostics.csproj
new file mode 100644
index 0000000..0c75626
--- /dev/null
+++ b/src/demo/XBase.Demo.Diagnostics/XBase.Demo.Diagnostics.csproj
@@ -0,0 +1,11 @@
+
+
+ XBase.Demo.Diagnostics
+ XBase.Demo.Diagnostics
+
+
+
+
+
+
+
diff --git a/src/demo/XBase.Demo.Domain/Catalog/CatalogModel.cs b/src/demo/XBase.Demo.Domain/Catalog/CatalogModel.cs
new file mode 100644
index 0000000..720f0c0
--- /dev/null
+++ b/src/demo/XBase.Demo.Domain/Catalog/CatalogModel.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+
+namespace XBase.Demo.Domain.Catalog;
+
+///
+/// Represents a catalog of xBase tables discovered under a root directory.
+///
+/// The file system root the catalog was scanned from.
+/// The set of tables available for browsing.
+public sealed record CatalogModel(string RootPath, IReadOnlyList Tables);
+
+///
+/// Represents a single table, including metadata necessary for browsing.
+///
+/// Logical table name.
+/// Absolute path to the DBF file.
+/// Associated indexes discovered for the table.
+public sealed record TableModel(string Name, string Path, IReadOnlyList Indexes);
+
+///
+/// Represents an index that can be applied while browsing.
+///
+/// Index identifier.
+/// The expression used to compute the index key.
+/// Optional ordering hint for display.
+public sealed record IndexModel(string Name, string Expression, int Order = 0);
diff --git a/src/demo/XBase.Demo.Domain/Catalog/TablePage.cs b/src/demo/XBase.Demo.Domain/Catalog/TablePage.cs
new file mode 100644
index 0000000..bf59989
--- /dev/null
+++ b/src/demo/XBase.Demo.Domain/Catalog/TablePage.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+
+namespace XBase.Demo.Domain.Catalog;
+
+///
+/// Represents a single page of table data for the browser grid.
+///
+/// The row set projected for the current page.
+/// Total row count matching the query.
+/// Zero-based page number.
+/// Maximum number of rows per page.
+public sealed record TablePage(IReadOnlyList> Rows, long TotalCount, int PageNumber, int PageSize);
+
+///
+/// Request descriptor for loading a page of table data.
+///
+/// Zero-based page number.
+/// Maximum number of rows per page.
+/// Optional sort expression to apply.
+/// Flag indicating whether deleted rows should be included.
+public sealed record TablePageRequest(int PageNumber, int PageSize, string? SortExpression = null, bool IncludeDeleted = false);
diff --git a/src/demo/XBase.Demo.Domain/Diagnostics/DemoTelemetryEvent.cs b/src/demo/XBase.Demo.Domain/Diagnostics/DemoTelemetryEvent.cs
new file mode 100644
index 0000000..3181677
--- /dev/null
+++ b/src/demo/XBase.Demo.Domain/Diagnostics/DemoTelemetryEvent.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+
+namespace XBase.Demo.Domain.Diagnostics;
+
+///
+/// Represents a telemetry event produced by the demo experience.
+///
+/// Event identifier.
+/// UTC timestamp when the event occurred.
+/// Structured payload for diagnostics dashboards.
+public sealed record DemoTelemetryEvent(string Name, DateTimeOffset Timestamp, IReadOnlyDictionary Payload);
+
+///
+/// Abstraction for publishing telemetry events to interested listeners.
+///
+public interface IDemoTelemetrySink
+{
+ void Publish(DemoTelemetryEvent telemetryEvent);
+}
diff --git a/src/demo/XBase.Demo.Domain/Services/ITableCatalogService.cs b/src/demo/XBase.Demo.Domain/Services/ITableCatalogService.cs
new file mode 100644
index 0000000..2718d2a
--- /dev/null
+++ b/src/demo/XBase.Demo.Domain/Services/ITableCatalogService.cs
@@ -0,0 +1,19 @@
+using System.Threading;
+using System.Threading.Tasks;
+using XBase.Demo.Domain.Catalog;
+
+namespace XBase.Demo.Domain.Services;
+
+///
+/// Provides catalog discovery and table metadata loading capabilities.
+///
+public interface ITableCatalogService
+{
+ ///
+ /// Scans the provided root directory for xBase table artifacts.
+ ///
+ /// The directory containing DBF/DBT/NTX assets.
+ /// Cancellation token for the asynchronous operation.
+ /// A populated describing the catalog.
+ Task LoadCatalogAsync(string rootPath, CancellationToken cancellationToken = default);
+}
diff --git a/src/demo/XBase.Demo.Domain/Services/ITablePageService.cs b/src/demo/XBase.Demo.Domain/Services/ITablePageService.cs
new file mode 100644
index 0000000..5a36c2f
--- /dev/null
+++ b/src/demo/XBase.Demo.Domain/Services/ITablePageService.cs
@@ -0,0 +1,13 @@
+using System.Threading;
+using System.Threading.Tasks;
+using XBase.Demo.Domain.Catalog;
+
+namespace XBase.Demo.Domain.Services;
+
+///
+/// Provides paginated access to table data for browsing scenarios.
+///
+public interface ITablePageService
+{
+ Task LoadPageAsync(TableModel table, TablePageRequest request, CancellationToken cancellationToken = default);
+}
diff --git a/src/demo/XBase.Demo.Domain/XBase.Demo.Domain.csproj b/src/demo/XBase.Demo.Domain/XBase.Demo.Domain.csproj
new file mode 100644
index 0000000..8ac6e15
--- /dev/null
+++ b/src/demo/XBase.Demo.Domain/XBase.Demo.Domain.csproj
@@ -0,0 +1,6 @@
+
+
+ XBase.Demo.Domain
+ XBase.Demo.Domain
+
+
diff --git a/src/demo/XBase.Demo.Infrastructure/Catalog/FileSystemTableCatalogService.cs b/src/demo/XBase.Demo.Infrastructure/Catalog/FileSystemTableCatalogService.cs
new file mode 100644
index 0000000..10cc9f7
--- /dev/null
+++ b/src/demo/XBase.Demo.Infrastructure/Catalog/FileSystemTableCatalogService.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Microsoft.Extensions.Logging;
+using XBase.Demo.Domain.Catalog;
+using XBase.Demo.Domain.Services;
+
+namespace XBase.Demo.Infrastructure.Catalog;
+
+///
+/// Minimal file-system backed implementation that surfaces DBF tables and common index artifacts.
+///
+public sealed class FileSystemTableCatalogService : ITableCatalogService
+{
+ private static readonly string[] SupportedIndexExtensions = [".ntx", ".mdx", ".ndx"];
+ private readonly ILogger _logger;
+
+ public FileSystemTableCatalogService(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public Task LoadCatalogAsync(string rootPath, CancellationToken cancellationToken = default)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(rootPath);
+
+ if (!Directory.Exists(rootPath))
+ {
+ throw new DirectoryNotFoundException($"Catalog root '{rootPath}' was not found.");
+ }
+
+ var absoluteRoot = Path.GetFullPath(rootPath);
+ var tables = new List();
+
+ foreach (var tableFile in Directory.EnumerateFiles(absoluteRoot, "*.dbf", SearchOption.TopDirectoryOnly))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var tableName = Path.GetFileNameWithoutExtension(tableFile);
+ var directory = Path.GetDirectoryName(tableFile)!;
+
+ var indexes = new List();
+ foreach (var indexFile in Directory.EnumerateFiles(directory, $"{tableName}.*", SearchOption.TopDirectoryOnly))
+ {
+ var extension = Path.GetExtension(indexFile);
+ if (!SupportedIndexExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ indexes.Add(new IndexModel(Path.GetFileName(indexFile), extension.ToUpperInvariant()));
+ }
+
+ tables.Add(new TableModel(tableName, tableFile, indexes));
+ }
+
+ _logger.LogInformation("Discovered {TableCount} tables under {RootPath}", tables.Count, absoluteRoot);
+ return Task.FromResult(new CatalogModel(absoluteRoot, tables));
+ }
+}
diff --git a/src/demo/XBase.Demo.Infrastructure/Catalog/NullTablePageService.cs b/src/demo/XBase.Demo.Infrastructure/Catalog/NullTablePageService.cs
new file mode 100644
index 0000000..8dd0fbd
--- /dev/null
+++ b/src/demo/XBase.Demo.Infrastructure/Catalog/NullTablePageService.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using XBase.Demo.Domain.Catalog;
+using XBase.Demo.Domain.Services;
+
+namespace XBase.Demo.Infrastructure.Catalog;
+
+///
+/// Temporary placeholder that returns empty pages until the data engine is wired up.
+///
+public sealed class NullTablePageService : ITablePageService
+{
+ public Task LoadPageAsync(TableModel table, TablePageRequest request, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(table);
+ ArgumentNullException.ThrowIfNull(request);
+
+ var emptyRows = Array.Empty>();
+ return Task.FromResult(new TablePage(emptyRows, 0, request.PageNumber, request.PageSize));
+ }
+}
diff --git a/src/demo/XBase.Demo.Infrastructure/ServiceCollectionExtensions.cs b/src/demo/XBase.Demo.Infrastructure/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..9e3c586
--- /dev/null
+++ b/src/demo/XBase.Demo.Infrastructure/ServiceCollectionExtensions.cs
@@ -0,0 +1,27 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using XBase.Demo.Domain.Services;
+using XBase.Demo.Infrastructure.Catalog;
+
+namespace XBase.Demo.Infrastructure;
+
+///
+/// Dependency injection helpers for the demo infrastructure layer.
+///
+public static class ServiceCollectionExtensions
+{
+ ///
+ /// Registers services necessary for the Phase A demo scenarios.
+ ///
+ public static IServiceCollection AddXBaseDemoInfrastructure(this IServiceCollection services)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+
+ services.AddLogging(builder => builder.AddDebug());
+ services.AddSingleton();
+ services.AddSingleton();
+
+ return services;
+ }
+}
diff --git a/src/demo/XBase.Demo.Infrastructure/XBase.Demo.Infrastructure.csproj b/src/demo/XBase.Demo.Infrastructure/XBase.Demo.Infrastructure.csproj
new file mode 100644
index 0000000..f7e3655
--- /dev/null
+++ b/src/demo/XBase.Demo.Infrastructure/XBase.Demo.Infrastructure.csproj
@@ -0,0 +1,14 @@
+
+
+ XBase.Demo.Infrastructure
+ XBase.Demo.Infrastructure
+
+
+
+
+
+
+
+
+
+
diff --git a/src/demo/tasks.md b/src/demo/tasks.md
new file mode 100644
index 0000000..dc7cb04
--- /dev/null
+++ b/src/demo/tasks.md
@@ -0,0 +1,25 @@
+# xBase Demo Solution Tasks
+
+Source blueprint: [docs/demo/avalonia-reactiveui-demo-plan.md](../../docs/demo/avalonia-reactiveui-demo-plan.md)
+
+## Milestone M1 – App Shell & Catalog Browser
+- [ ] Scaffold Avalonia desktop host with ReactiveUI composition root.
+- [ ] Implement catalog scanning service and basic directory selection flow.
+- [ ] Build table/index listing view models with paging placeholders.
+- [ ] Wire diagnostics/logging sinks and sample telemetry events.
+- [ ] Add integration smoke harness once basic navigation is ready.
+
+## Milestone M2 – DDL & Index Basics
+- [ ] Generate DDL preview scripts for create/alter/drop operations.
+- [ ] Implement index create/drop services with error surfacing.
+- [ ] Extend ViewModels with command handlers for schema updates.
+
+## Milestone M3 – Rebuild & Diagnostics
+- [ ] Add side-by-side index rebuild orchestration with progress observables.
+- [ ] Surface performance metrics and index selection feedback in UI.
+- [ ] Expand diagnostics feed for journal/index events.
+
+## Milestone M4 – Seed & Recovery Demo
+- [ ] Implement CSV import pipeline with encoding detection.
+- [ ] Simulate crash scenarios and recovery replay workflow.
+- [ ] Publish diagnostic/export reports for support packages.
diff --git a/xBase.sln b/xBase.sln
index e785a56..34af54a 100644
--- a/xBase.sln
+++ b/xBase.sln
@@ -35,6 +35,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XBase.Diagnostics.Tests", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XBase.Tools.Tests", "tests\XBase.Tools.Tests\XBase.Tools.Tests.csproj", "{2B8B378E-3016-4964-89A4-539C279B0D5A}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "demo", "demo", "{8216414C-D70B-47BF-8296-90A08F02D90C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XBase.Demo.Domain", "src\demo\XBase.Demo.Domain\XBase.Demo.Domain.csproj", "{D83CDBB4-E216-4D63-8F77-8193C185366A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XBase.Demo.Infrastructure", "src\demo\XBase.Demo.Infrastructure\XBase.Demo.Infrastructure.csproj", "{25B73D78-0BB2-480D-8E6B-CAF36DE60690}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XBase.Demo.Diagnostics", "src\demo\XBase.Demo.Diagnostics\XBase.Demo.Diagnostics.csproj", "{4C647419-449C-4B34-828A-103D6B7F8AAE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XBase.Demo.App", "src\demo\XBase.Demo.App\XBase.Demo.App.csproj", "{1E80FE92-7098-4406-914A-24CE7CBED4AC}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -100,6 +110,22 @@ Global
{2B8B378E-3016-4964-89A4-539C279B0D5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2B8B378E-3016-4964-89A4-539C279B0D5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2B8B378E-3016-4964-89A4-539C279B0D5A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D83CDBB4-E216-4D63-8F77-8193C185366A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D83CDBB4-E216-4D63-8F77-8193C185366A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D83CDBB4-E216-4D63-8F77-8193C185366A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D83CDBB4-E216-4D63-8F77-8193C185366A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {25B73D78-0BB2-480D-8E6B-CAF36DE60690}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {25B73D78-0BB2-480D-8E6B-CAF36DE60690}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {25B73D78-0BB2-480D-8E6B-CAF36DE60690}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {25B73D78-0BB2-480D-8E6B-CAF36DE60690}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4C647419-449C-4B34-828A-103D6B7F8AAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4C647419-449C-4B34-828A-103D6B7F8AAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4C647419-449C-4B34-828A-103D6B7F8AAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4C647419-449C-4B34-828A-103D6B7F8AAE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1E80FE92-7098-4406-914A-24CE7CBED4AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1E80FE92-7098-4406-914A-24CE7CBED4AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1E80FE92-7098-4406-914A-24CE7CBED4AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1E80FE92-7098-4406-914A-24CE7CBED4AC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{B2E0A9D6-E39E-44E6-9762-B1A5C50D29B6} = {DB34B0F0-48E5-441D-B062-A525C37FD21D}
@@ -116,5 +142,10 @@ Global
{659059E0-B3CE-44D2-B68A-01227B398A1D} = {8944503E-D8CB-4870-A577-E3C76244B9F6}
{ADBFA642-7C6C-43E7-A9B2-F4B3473B9FC5} = {8944503E-D8CB-4870-A577-E3C76244B9F6}
{2B8B378E-3016-4964-89A4-539C279B0D5A} = {8944503E-D8CB-4870-A577-E3C76244B9F6}
+ {8216414C-D70B-47BF-8296-90A08F02D90C} = {DB34B0F0-48E5-441D-B062-A525C37FD21D}
+ {D83CDBB4-E216-4D63-8F77-8193C185366A} = {8216414C-D70B-47BF-8296-90A08F02D90C}
+ {25B73D78-0BB2-480D-8E6B-CAF36DE60690} = {8216414C-D70B-47BF-8296-90A08F02D90C}
+ {4C647419-449C-4B34-828A-103D6B7F8AAE} = {8216414C-D70B-47BF-8296-90A08F02D90C}
+ {1E80FE92-7098-4406-914A-24CE7CBED4AC} = {8216414C-D70B-47BF-8296-90A08F02D90C}
EndGlobalSection
EndGlobal