Files
Sharepoint-Toolbox/.planning/phases/04-bulk-operations-and-provisioning/04-10-PLAN.md
Dev d73e50948d docs(04): create Phase 4 plan — 10 plans for Bulk Operations and Provisioning
Wave 0: models, interfaces, BulkOperationRunner, test scaffolds
Wave 1: CsvValidationService, TemplateRepository, FileTransferService,
        BulkMemberService, BulkSiteService, TemplateService, FolderStructureService
Wave 2: Localization, shared dialogs, example CSV resources
Wave 3: TransferVM+View, BulkMembers/BulkSites/FolderStructure VMs+Views,
        TemplatesVM+View, DI registration, MainWindow wiring

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 09:38:33 +02:00

24 KiB

phase, plan, title, status, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan title status wave depends_on files_modified autonomous requirements must_haves
04 10 TemplatesViewModel + TemplatesView + DI Registration + MainWindow Wiring pending 3
04-02
04-06
04-07
04-08
04-09
SharepointToolbox/ViewModels/Tabs/TemplatesViewModel.cs
SharepointToolbox/Views/Tabs/TemplatesView.xaml
SharepointToolbox/Views/Tabs/TemplatesView.xaml.cs
SharepointToolbox/App.xaml.cs
SharepointToolbox/MainWindow.xaml
SharepointToolbox/MainWindow.xaml.cs
false
TMPL-01
TMPL-02
TMPL-03
TMPL-04
truths artifacts key_links
TemplatesView shows a list of saved templates with capture, apply, rename, delete buttons
User can capture a template from a connected site with checkbox options
User can apply a template to create a new site
All Phase 4 services, ViewModels, and Views are registered in DI
All 5 new tabs appear in MainWindow (Transfer, Bulk Members, Bulk Sites, Folder Structure, Templates)
Application launches and all tabs are visible
path provides exports
SharepointToolbox/ViewModels/Tabs/TemplatesViewModel.cs Templates tab ViewModel
TemplatesViewModel
path provides
SharepointToolbox/Views/Tabs/TemplatesView.xaml Templates tab UI
path provides
SharepointToolbox/App.xaml.cs DI registration for all Phase 4 types
path provides
SharepointToolbox/MainWindow.xaml 5 new tab items replacing FeatureTabBase stubs
from to via pattern
TemplatesViewModel.cs ITemplateService capture and apply operations CaptureTemplateAsync|ApplyTemplateAsync
from to via pattern
TemplatesViewModel.cs TemplateRepository template CRUD TemplateRepository
from to via pattern
App.xaml.cs All Phase 4 services DI registration AddTransient
from to via pattern
MainWindow.xaml.cs All Phase 4 Views tab content wiring GetRequiredService

Plan 04-10: TemplatesViewModel + TemplatesView + DI Registration + MainWindow Wiring

Goal

Create the Templates tab (ViewModel + View), register ALL Phase 4 services/ViewModels/Views in DI, wire all 5 new tabs in MainWindow, and verify the app launches with all tabs visible.

Context

All services are implemented: FileTransferService (04-03), BulkMemberService (04-04), BulkSiteService (04-05), TemplateService + FolderStructureService (04-06), CsvValidationService (04-02), TemplateRepository (04-02). All ViewModels/Views for Transfer (04-08), BulkMembers/BulkSites/FolderStructure (04-09) are done.

DI pattern: Services as AddTransient<Interface, Implementation>(). ViewModels/Views as AddTransient<Type>(). Infrastructure singletons as AddSingleton<Type>(). Register in App.xaml.cs RegisterServices().

MainWindow pattern: Add x:Name TabItems in XAML, set Content from DI in code-behind constructor.

Current MainWindow.xaml has 3 stub tabs (Templates, Bulk, Structure) with FeatureTabBase. These must be replaced with the 5 new named TabItems.

Tasks

Task 1: Create TemplatesViewModel + TemplatesView

Files:

  • SharepointToolbox/ViewModels/Tabs/TemplatesViewModel.cs
  • SharepointToolbox/Views/Tabs/TemplatesView.xaml
  • SharepointToolbox/Views/Tabs/TemplatesView.xaml.cs

Action:

  1. Create TemplatesViewModel.cs:
using System.Collections.ObjectModel;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using Serilog;
using SharepointToolbox.Core.Models;
using SharepointToolbox.Infrastructure.Persistence;
using SharepointToolbox.Localization;
using SharepointToolbox.Services;

namespace SharepointToolbox.ViewModels.Tabs;

public partial class TemplatesViewModel : FeatureViewModelBase
{
    private readonly ITemplateService _templateService;
    private readonly TemplateRepository _templateRepo;
    private readonly ISessionManager _sessionManager;
    private readonly ILogger<FeatureViewModelBase> _logger;
    private TenantProfile? _currentProfile;

    // Template list
    private ObservableCollection<SiteTemplate> _templates = new();
    public ObservableCollection<SiteTemplate> Templates
    {
        get => _templates;
        private set { _templates = value; OnPropertyChanged(); }
    }

    [ObservableProperty] private SiteTemplate? _selectedTemplate;

    // Capture options
    [ObservableProperty] private string _captureSiteUrl = string.Empty;
    [ObservableProperty] private string _templateName = string.Empty;
    [ObservableProperty] private bool _captureLibraries = true;
    [ObservableProperty] private bool _captureFolders = true;
    [ObservableProperty] private bool _capturePermissions = true;
    [ObservableProperty] private bool _captureLogo = true;
    [ObservableProperty] private bool _captureSettings = true;

    // Apply options
    [ObservableProperty] private string _newSiteTitle = string.Empty;
    [ObservableProperty] private string _newSiteAlias = string.Empty;

    public IAsyncRelayCommand CaptureCommand { get; }
    public IAsyncRelayCommand ApplyCommand { get; }
    public IAsyncRelayCommand RenameCommand { get; }
    public IAsyncRelayCommand DeleteCommand { get; }
    public IAsyncRelayCommand RefreshCommand { get; }

    public TenantProfile? CurrentProfile => _currentProfile;

    public TemplatesViewModel(
        ITemplateService templateService,
        TemplateRepository templateRepo,
        ISessionManager sessionManager,
        ILogger<FeatureViewModelBase> logger)
        : base(logger)
    {
        _templateService = templateService;
        _templateRepo = templateRepo;
        _sessionManager = sessionManager;
        _logger = logger;

        CaptureCommand = new AsyncRelayCommand(CaptureAsync, () => !IsRunning);
        ApplyCommand = new AsyncRelayCommand(ApplyAsync, () => !IsRunning && SelectedTemplate != null);
        RenameCommand = new AsyncRelayCommand(RenameAsync, () => SelectedTemplate != null);
        DeleteCommand = new AsyncRelayCommand(DeleteAsync, () => SelectedTemplate != null);
        RefreshCommand = new AsyncRelayCommand(RefreshListAsync);
    }

    protected override async Task RunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress)
    {
        // Not used directly — Capture and Apply have their own async commands
        await Task.CompletedTask;
    }

    private async Task CaptureAsync()
    {
        if (_currentProfile == null)
            throw new InvalidOperationException("No tenant connected.");
        if (string.IsNullOrWhiteSpace(CaptureSiteUrl))
            throw new InvalidOperationException("Site URL is required.");
        if (string.IsNullOrWhiteSpace(TemplateName))
            throw new InvalidOperationException("Template name is required.");

        try
        {
            IsRunning = true;
            StatusMessage = "Capturing template...";

            var profile = new TenantProfile
            {
                Name = _currentProfile.Name,
                TenantUrl = CaptureSiteUrl,
                ClientId = _currentProfile.ClientId,
            };
            var ctx = await _sessionManager.GetOrCreateContextAsync(profile, CancellationToken.None);

            var options = new SiteTemplateOptions
            {
                CaptureLibraries = CaptureLibraries,
                CaptureFolders = CaptureFolders,
                CapturePermissionGroups = CapturePermissions,
                CaptureLogo = CaptureLogo,
                CaptureSettings = CaptureSettings,
            };

            var progress = new Progress<OperationProgress>(p => StatusMessage = p.Message);
            var template = await _templateService.CaptureTemplateAsync(ctx, options, progress, CancellationToken.None);
            template.Name = TemplateName;

            await _templateRepo.SaveAsync(template);
            Log.Information("Template captured: {Name} from {Url}", template.Name, CaptureSiteUrl);

            await RefreshListAsync();
            StatusMessage = $"Template '{TemplateName}' captured successfully.";
        }
        catch (Exception ex)
        {
            StatusMessage = $"Capture failed: {ex.Message}";
            Log.Error(ex, "Template capture failed");
        }
        finally
        {
            IsRunning = false;
        }
    }

    private async Task ApplyAsync()
    {
        if (_currentProfile == null || SelectedTemplate == null) return;
        if (string.IsNullOrWhiteSpace(NewSiteTitle))
            throw new InvalidOperationException("New site title is required.");
        if (string.IsNullOrWhiteSpace(NewSiteAlias))
            throw new InvalidOperationException("New site alias is required.");

        try
        {
            IsRunning = true;
            StatusMessage = $"Applying template '{SelectedTemplate.Name}'...";

            var ctx = await _sessionManager.GetOrCreateContextAsync(_currentProfile, CancellationToken.None);
            var progress = new Progress<OperationProgress>(p => StatusMessage = p.Message);

            var siteUrl = await _templateService.ApplyTemplateAsync(
                ctx, SelectedTemplate, NewSiteTitle, NewSiteAlias,
                progress, CancellationToken.None);

            StatusMessage = $"Template applied. Site created at: {siteUrl}";
            Log.Information("Template '{Name}' applied. New site: {Url}", SelectedTemplate.Name, siteUrl);
        }
        catch (Exception ex)
        {
            StatusMessage = $"Apply failed: {ex.Message}";
            Log.Error(ex, "Template apply failed");
        }
        finally
        {
            IsRunning = false;
        }
    }

    private async Task RenameAsync()
    {
        if (SelectedTemplate == null) return;

        // Simple input dialog — use a prompt via code-behind or InputBox
        // The View will wire this via a Func<string, string?> factory
        if (RenameDialogFactory != null)
        {
            var newName = RenameDialogFactory(SelectedTemplate.Name);
            if (!string.IsNullOrWhiteSpace(newName))
            {
                await _templateRepo.RenameAsync(SelectedTemplate.Id, newName);
                await RefreshListAsync();
                Log.Information("Template renamed: {OldName} -> {NewName}", SelectedTemplate.Name, newName);
            }
        }
    }

    private async Task DeleteAsync()
    {
        if (SelectedTemplate == null) return;

        await _templateRepo.DeleteAsync(SelectedTemplate.Id);
        await RefreshListAsync();
        Log.Information("Template deleted: {Name}", SelectedTemplate.Name);
    }

    private async Task RefreshListAsync()
    {
        var templates = await _templateRepo.GetAllAsync();
        await Application.Current.Dispatcher.InvokeAsync(() =>
        {
            Templates = new ObservableCollection<SiteTemplate>(templates);
        });
    }

    // Factory for rename dialog — set by View code-behind
    public Func<string, string?>? RenameDialogFactory { get; set; }

    protected override void OnTenantSwitched(TenantProfile profile)
    {
        _currentProfile = profile;
        CaptureSiteUrl = string.Empty;
        TemplateName = string.Empty;
        NewSiteTitle = string.Empty;
        NewSiteAlias = string.Empty;
        StatusMessage = string.Empty;

        // Refresh template list on tenant switch
        _ = RefreshListAsync();
    }

    partial void OnSelectedTemplateChanged(SiteTemplate? value)
    {
        ApplyCommand.NotifyCanExecuteChanged();
        RenameCommand.NotifyCanExecuteChanged();
        DeleteCommand.NotifyCanExecuteChanged();
    }
}
  1. Create TemplatesView.xaml:
<UserControl x:Class="SharepointToolbox.Views.Tabs.TemplatesView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:loc="clr-namespace:SharepointToolbox.Localization">
    <DockPanel Margin="10">
        <!-- Left panel: Capture and Apply -->
        <StackPanel DockPanel.Dock="Left" Width="320" Margin="0,0,10,0">
            <!-- Capture Section -->
            <GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.capture]}"
                      Margin="0,0,0,10">
                <StackPanel Margin="5">
                    <TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.siteurl]}"
                               Margin="0,0,0,3" />
                    <TextBox Text="{Binding CaptureSiteUrl, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,5" />

                    <TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.name]}"
                               Margin="0,0,0,3" />
                    <TextBox Text="{Binding TemplateName, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,10" />

                    <!-- Capture options checkboxes -->
                    <TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.options]}"
                               FontWeight="SemiBold" Margin="0,0,0,5" />
                    <CheckBox Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.opt.libraries]}"
                              IsChecked="{Binding CaptureLibraries}" Margin="0,0,0,3" />
                    <CheckBox Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.opt.folders]}"
                              IsChecked="{Binding CaptureFolders}" Margin="0,0,0,3" />
                    <CheckBox Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.opt.permissions]}"
                              IsChecked="{Binding CapturePermissions}" Margin="0,0,0,3" />
                    <CheckBox Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.opt.logo]}"
                              IsChecked="{Binding CaptureLogo}" Margin="0,0,0,3" />
                    <CheckBox Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.opt.settings]}"
                              IsChecked="{Binding CaptureSettings}" Margin="0,0,0,10" />

                    <Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.capture]}"
                            Command="{Binding CaptureCommand}" />
                </StackPanel>
            </GroupBox>

            <!-- Apply Section -->
            <GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.apply]}"
                      Margin="0,0,0,10">
                <StackPanel Margin="5">
                    <TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.newtitle]}"
                               Margin="0,0,0,3" />
                    <TextBox Text="{Binding NewSiteTitle, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,5" />

                    <TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.newalias]}"
                               Margin="0,0,0,3" />
                    <TextBox Text="{Binding NewSiteAlias, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,10" />

                    <Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.apply]}"
                            Command="{Binding ApplyCommand}" />
                </StackPanel>
            </GroupBox>

            <!-- Progress -->
            <TextBlock Text="{Binding StatusMessage}" TextWrapping="Wrap" Margin="0,5,0,0" />
        </StackPanel>

        <!-- Right panel: Template list -->
        <DockPanel>
            <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,5">
                <TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.list]}"
                           FontWeight="Bold" FontSize="14" VerticalAlignment="Center" Margin="0,0,10,0" />
                <Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.rename]}"
                        Command="{Binding RenameCommand}" Margin="0,0,5,0" Padding="10,3" />
                <Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.delete]}"
                        Command="{Binding DeleteCommand}" Padding="10,3" />
            </StackPanel>

            <DataGrid ItemsSource="{Binding Templates}" SelectedItem="{Binding SelectedTemplate}"
                      AutoGenerateColumns="False" IsReadOnly="True"
                      SelectionMode="Single" CanUserSortColumns="True">
                <DataGrid.Columns>
                    <DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="*" />
                    <DataGridTextColumn Header="Type" Binding="{Binding SiteType}" Width="100" />
                    <DataGridTextColumn Header="Source" Binding="{Binding SourceUrl}" Width="*" />
                    <DataGridTextColumn Header="Captured" Binding="{Binding CapturedAt, StringFormat=yyyy-MM-dd HH:mm}" Width="140" />
                </DataGrid.Columns>
            </DataGrid>
        </DockPanel>
    </DockPanel>
</UserControl>
  1. Create TemplatesView.xaml.cs:
using System.Windows;
using System.Windows.Controls;
using Microsoft.VisualBasic;

namespace SharepointToolbox.Views.Tabs;

public partial class TemplatesView : UserControl
{
    public TemplatesView(ViewModels.Tabs.TemplatesViewModel viewModel)
    {
        InitializeComponent();
        DataContext = viewModel;

        // Wire rename dialog factory — use simple InputBox
        viewModel.RenameDialogFactory = currentName =>
        {
            // Simple prompt — WPF has no built-in InputBox, use Microsoft.VisualBasic.Interaction.InputBox
            // or create a simple dialog. For simplicity, use a MessageBox approach.
            var result = Microsoft.VisualBasic.Interaction.InputBox(
                "Enter new template name:", "Rename Template", currentName);
            return string.IsNullOrWhiteSpace(result) ? null : result;
        };

        // Load templates on first display
        viewModel.RefreshCommand.ExecuteAsync(null);
    }
}

Note: If Microsoft.VisualBasic is not available or undesired, create a simple InputDialog Window instead. The executor should check if Microsoft.VisualBasic is referenced (it's part of .NET SDK by default) or create a minimal WPF dialog.

Verify:

dotnet build SharepointToolbox/SharepointToolbox.csproj --no-restore -q

Done: TemplatesViewModel and TemplatesView compile. Template list, capture with checkboxes, apply with title/alias, rename, delete all connected.

Task 2: Register all Phase 4 types in DI + Wire MainWindow tabs

Files:

  • SharepointToolbox/App.xaml.cs
  • SharepointToolbox/MainWindow.xaml
  • SharepointToolbox/MainWindow.xaml.cs

Action:

  1. Update App.xaml.cs — add Phase 4 DI registrations in RegisterServices(), after the existing Phase 3 block:
// Add these using statements at the top:
using SharepointToolbox.Infrastructure.Auth;
// (other usings already present)

// Add in RegisterServices(), after Phase 3 block:

// Phase 4: Bulk Operations Infrastructure
var templatesDir = Path.Combine(appData, "templates");
services.AddSingleton(_ => new TemplateRepository(templatesDir));
services.AddSingleton<GraphClientFactory>();
services.AddTransient<ICsvValidationService, CsvValidationService>();
services.AddTransient<BulkResultCsvExportService>();

// Phase 4: File Transfer
services.AddTransient<IFileTransferService, FileTransferService>();
services.AddTransient<TransferViewModel>();
services.AddTransient<TransferView>();

// Phase 4: Bulk Members
services.AddTransient<IBulkMemberService, BulkMemberService>();
services.AddTransient<BulkMembersViewModel>();
services.AddTransient<BulkMembersView>();

// Phase 4: Bulk Sites
services.AddTransient<IBulkSiteService, BulkSiteService>();
services.AddTransient<BulkSitesViewModel>();
services.AddTransient<BulkSitesView>();

// Phase 4: Templates
services.AddTransient<ITemplateService, TemplateService>();
services.AddTransient<TemplatesViewModel>();
services.AddTransient<TemplatesView>();

// Phase 4: Folder Structure
services.AddTransient<IFolderStructureService, FolderStructureService>();
services.AddTransient<FolderStructureViewModel>();
services.AddTransient<FolderStructureView>();

Also add required using statements at top of App.xaml.cs:

using SharepointToolbox.Infrastructure.Auth;  // GraphClientFactory
// Other new usings should be covered by existing namespace imports
  1. Update MainWindow.xaml — replace the 3 FeatureTabBase stub tabs (Templates, Bulk, Structure) with 5 named TabItems:

Replace:

<TabItem Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.templates]}">
    <controls:FeatureTabBase />
</TabItem>
<TabItem Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.bulk]}">
    <controls:FeatureTabBase />
</TabItem>
<TabItem Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.structure]}">
    <controls:FeatureTabBase />
</TabItem>

With:

<TabItem x:Name="TransferTabItem"
         Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.transfer]}">
</TabItem>
<TabItem x:Name="BulkMembersTabItem"
         Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.bulkMembers]}">
</TabItem>
<TabItem x:Name="BulkSitesTabItem"
         Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.bulkSites]}">
</TabItem>
<TabItem x:Name="FolderStructureTabItem"
         Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.folderStructure]}">
</TabItem>
<TabItem x:Name="TemplatesTabItem"
         Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.templates]}">
</TabItem>

Note: Keep the Settings tab at the end. The tab order should be: Permissions, Storage, Search, Duplicates, Transfer, Bulk Members, Bulk Sites, Folder Structure, Templates, Settings.

  1. Update MainWindow.xaml.cs — add tab content wiring in the constructor, after existing tab assignments:
// Add after existing DuplicatesTabItem.Content line:

// Phase 4: Replace stub tabs with DI-resolved Views
TransferTabItem.Content = serviceProvider.GetRequiredService<TransferView>();
BulkMembersTabItem.Content = serviceProvider.GetRequiredService<BulkMembersView>();
BulkSitesTabItem.Content = serviceProvider.GetRequiredService<BulkSitesView>();
FolderStructureTabItem.Content = serviceProvider.GetRequiredService<FolderStructureView>();
TemplatesTabItem.Content = serviceProvider.GetRequiredService<TemplatesView>();

Verify:

dotnet build SharepointToolbox.slnx --no-restore -q && dotnet test SharepointToolbox.Tests --no-build -q

Done: All Phase 4 services, ViewModels, and Views registered in DI. All 5 new tabs wired in MainWindow. Application builds and all tests pass.

Task 3: Visual checkpoint

Type: checkpoint:human-verify

What-built: All 5 Phase 4 tabs (Transfer, Bulk Members, Bulk Sites, Folder Structure, Templates) integrated into the application.

How-to-verify:

  1. Run the application: dotnet run --project SharepointToolbox/SharepointToolbox.csproj
  2. Verify all 10 tabs are visible: Permissions, Storage, Search, Duplicates, Transfer, Bulk Members, Bulk Sites, Folder Structure, Templates, Settings
  3. Click each new tab — verify it shows the expected layout (no crash, no blank tab)
  4. On Bulk Members tab: click "Load Example" — verify the DataGrid populates with sample member data
  5. On Bulk Sites tab: click "Load Example" — verify the DataGrid populates with sample site data
  6. On Folder Structure tab: click "Load Example" — verify the DataGrid populates with folder structure data
  7. On Templates tab: verify the capture options section shows 5 checkboxes (Libraries, Folders, Permission Groups, Logo, Settings)
  8. On Transfer tab: verify source/destination sections with Browse buttons are visible

Resume-signal: Type "approved" or describe issues.

Commit: feat(04-10): register Phase 4 DI + wire MainWindow tabs + TemplatesView