refactor(configmanager): simplify SecureStore UI with unified info view

Consolidate SecureStoreLockedFormView and SecureStoreUnlockedFormView into
a single SecureStoreInfoFormView that displays store status and metadata.
Simplifies MainWindowViewModel by removing redundant state management.
Also adds design docs for RegexTransformer feature.
This commit is contained in:
Joseph Doherty
2026-01-22 09:40:38 -05:00
parent 5669bac221
commit 9bf0c29add
28 changed files with 2811 additions and 1527 deletions
@@ -0,0 +1,216 @@
# Regex Transformer Design
**Date:** 2025-01-22
**Status:** Approved
**Author:** Claude + User collaboration
## Overview
Add a new `RegexTransformer` to the DataSync ETL pipeline that transforms string values in a column using regular expressions. Includes a custom editor for the ConfigManager with a live test/preview feature.
## Requirements
1. **Two transformation modes:**
- **Find & Replace** - Replace matched text with replacement string
- **Match & Extract** - Extract first capture group from pattern
2. **Single column per transformer** - Each transformer operates on one column; add multiple transformers for multiple columns
3. **Configurable non-match behavior:**
- Keep original value (default)
- Return null
- Return empty string
4. **Case-insensitive option** - Optional flag for case-insensitive matching
5. **Test/preview in editor** - Users can test their regex pattern against sample input before saving
## Architecture
### Core Transformer
**File:** `NEW/src/JdeScoping.DataSync/Etl/Transformers/RegexTransformer.cs`
```csharp
public enum NonMatchBehavior
{
KeepOriginal,
ReturnNull,
ReturnEmpty
}
public class RegexTransformer : DataTransformerBase
{
public RegexTransformer(
string columnName,
string pattern,
string? replacement = null, // null = Match & Extract mode
bool ignoreCase = false,
NonMatchBehavior nonMatchBehavior = NonMatchBehavior.KeepOriginal)
}
```
**Behavior:**
- Extends `DataTransformerBase`
- In `OnInitialize()`: finds column ordinal, compiles regex
- Overrides `GetValue()`: transforms target column, passes through others
- Mode determined by `replacement` parameter: non-null = Find & Replace, null = Match & Extract
**Transformation logic:**
1. If not target column → pass through
2. If null/DBNull → pass through
3. Find & Replace mode: `regex.Replace(value, replacement)`
4. Match & Extract mode: return `match.Groups[1].Value` if match, else apply `NonMatchBehavior`
### Configuration Model
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Models/PipelineModel.cs`
Add enum:
```csharp
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum NonMatchBehavior
{
KeepOriginal,
ReturnNull,
ReturnEmpty
}
```
Add properties to `TransformerModel`:
```csharp
public string? ColumnName { get; set; }
public string? Pattern { get; set; }
public string? Replacement { get; set; }
public bool IgnoreCase { get; set; }
public NonMatchBehavior NonMatchBehavior { get; set; } = NonMatchBehavior.KeepOriginal;
```
**JSON example:**
```json
{
"Type": "Regex",
"ColumnName": "BatchID",
"Pattern": "^IIS_",
"Replacement": "",
"IgnoreCase": true,
"NonMatchBehavior": "KeepOriginal"
}
```
### ViewModel
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/PipelineSteps/TransformerStepViewModels.cs`
Add `RegexTransformerViewModel` class:
**Core properties (bound to config):**
- `ColumnName` (string)
- `Pattern` (string)
- `Replacement` (string)
- `IgnoreCase` (bool)
- `NonMatchBehavior` (enum)
**Mode properties (computed):**
- `IsFindReplaceMode` / `IsMatchExtractMode` - for radio button binding
- `PatternHelpText` - changes based on mode
**Test feature properties:**
- `TestInput` (string)
- `TestResultValue` (string)
- `TestResultLabel` (string) - "Output" or "No Match"
- `TestResultIcon` (string) - "✓" or "—"
- `TestResultBackground` (string) - green or orange
- `HasTestResult` / `HasTestError` (bool)
- `TestErrorMessage` (string)
**Command:**
- `TestPatternCommand` - executes regex test
### Editor View
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Views/Editors/RegexEditorView.axaml`
Layout sections:
1. **Header** - Title "Regex Transformer" + description
2. **Column Name** - Text input with required indicator
3. **Mode Selection** - Radio buttons: "Find & Replace" / "Match & Extract"
4. **Pattern** - Monospace text input with dynamic help text
5. **Replacement** - Monospace text input (visible only in Find & Replace mode)
6. **Options Row** - Case Insensitive checkbox + Non-Match Behavior dropdown
7. **Test Section** - Bordered area with:
- Sample input textbox
- Test button (blue)
- Result display with status icon (green checkmark / orange dash)
- Error display (red-tinted) for invalid patterns
8. **Help Box** - Pattern examples
**Design tokens (matching existing editors):**
- Background: `#0D0F12`, `#151920`, `#232A35`
- Text: `#E6EDF5` (bright), `#9BA8B8` (labels), `#5C6A7A` (dim)
- Accent: `#3B82F6` (blue button), `#22C55E` (success), `#F59E0B` (warning), `#FF6B6B` (error)
- Font: JetBrains Mono for code fields
- Spacing: 16px between sections, 4px within field groups
### Registration
**TransformerFactory updates:**
```csharp
// Create() switch:
"regex" => new RegexTransformerViewModel(model, onChanged),
// CreateNew() switch:
"regex" => new RegexTransformerViewModel(onChanged),
// AvailableTypes:
["ColumnDrop", "ColumnRename", "JdeDate", "Regex"]
```
**MainWindow.axaml DataTemplate:**
```xml
<DataTemplate DataType="{x:Type steps:RegexTransformerViewModel}">
<editors:RegexEditorView/>
</DataTemplate>
```
## Testing Strategy
### RegexTransformer Unit Tests
| Test Case | Description |
|-----------|-------------|
| `FindReplace_RemovesPrefix` | `^IIS_` + `""``IIS_12345` becomes `12345` |
| `FindReplace_ReplacesWithText` | `foo` + `bar``foo123` becomes `bar123` |
| `FindReplace_UseCaptureGroups` | `(\d+)-(\d+)` + `$2-$1` swaps groups |
| `MatchExtract_ExtractsFirstGroup` | `ID_(\d+)` extracts `12345` from `ID_12345` |
| `MatchExtract_NoMatch_KeepOriginal` | Returns original when no match |
| `MatchExtract_NoMatch_ReturnNull` | Returns DBNull when configured |
| `MatchExtract_NoMatch_ReturnEmpty` | Returns `""` when configured |
| `IgnoreCase_MatchesDifferentCase` | `^iis_` matches `IIS_12345` |
| `NullValue_PassesThrough` | DBNull input returns DBNull |
| `NonTargetColumn_Unchanged` | Other columns pass through |
| `InvalidRegex_ThrowsOnInitialize` | Bad pattern throws meaningful exception |
### RegexTransformerViewModel Unit Tests
| Test Case | Description |
|-----------|-------------|
| `TestPattern_ValidRegex_ShowsResult` | Test button displays transformed output |
| `TestPattern_InvalidRegex_ShowsError` | Bad pattern shows error message |
| `ModeSwitch_UpdatesHelpText` | Help text changes with mode |
| `ToModel_SerializesCorrectly` | ViewModel produces valid TransformerModel |
| `FromModel_LoadsAllProperties` | Constructor populates from existing config |
## Files Summary
**Create:**
- `NEW/src/JdeScoping.DataSync/Etl/Transformers/RegexTransformer.cs`
- `NEW/src/Utils/JdeScoping.ConfigManager/Views/Editors/RegexEditorView.axaml`
- `NEW/src/Utils/JdeScoping.ConfigManager/Views/Editors/RegexEditorView.axaml.cs`
- `NEW/tests/JdeScoping.DataSync.Tests/Etl/Transformers/RegexTransformerTests.cs`
- `NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/RegexTransformerViewModelTests.cs`
**Modify:**
- `NEW/src/Utils/JdeScoping.ConfigManager/Models/PipelineModel.cs` - Add enum + properties
- `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/PipelineSteps/TransformerStepViewModels.cs` - Add ViewModel + factory
- `NEW/src/Utils/JdeScoping.ConfigManager/Views/MainWindow.axaml` - Add DataTemplate
File diff suppressed because it is too large Load Diff
+525
View File
@@ -0,0 +1,525 @@
# Implementation Plan: SecureStore Auto-Initialization & Tree Flattening
## Overview
This plan implements automatic SecureStore initialization on config load and flattens the tree hierarchy from "Secure Stores > secrets > [keys]" to "Secure Store > [keys]".
## Design Decisions (from brainstorming)
1. **Auto-creation**: Fully automatic on config folder load when `AutoCreateStore=true`
2. **Path updates**: Write actual paths back to appsettings.json after creating files
3. **Tree structure**: Single "Secure Store" node with secrets as direct children
4. **Authentication**: Keyfile-only (remove password option entirely)
5. **Lock concept**: Removed - store is always open while config folder is loaded
6. **Icon**: Key icon (🔑) for "Secure Store" node
7. **Right panel**: Show instructions "Select a secret to edit" when store node selected
8. **Error handling**: Show error dialog if keyfile missing/corrupted
---
## Task 1: Update TreeNodeType enum
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/TreeNodeViewModel.cs`
**Changes:**
- Remove `SecureStoresFolder` enum value (no longer needed)
- Keep `SecureStore` for the single store node
- Keep `Secret` for individual secrets
**Before (lines 5-13):**
```csharp
public enum TreeNodeType
{
Folder,
SettingsSection,
Pipeline,
SecureStoresFolder, // The "Secure Stores" folder
SecureStore, // Individual store files
Secret // Individual secrets within a store
}
```
**After:**
```csharp
public enum TreeNodeType
{
Folder,
SettingsSection,
Pipeline,
SecureStore, // The single secure store node
Secret // Individual secrets within a store
}
```
---
## Task 2: Remove lock-related properties from TreeNodeViewModel
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/TreeNodeViewModel.cs`
**Remove these properties (lines 55-79):**
- `IsUnlocked` property and backing field
- `IsLocked` computed property
- `LockIcon` computed property
**Keep:**
- `StorePath` and `KeyFilePath` (needed for store identification)
- `SecretKey` (needed for secret nodes)
---
## Task 3: Remove password-related methods from ISecureStoreManager
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/ISecureStoreManager.cs`
**Remove these methods:**
- `CreateStoreWithPassword(string storePath, string password)` (lines 30-35)
- `OpenStoreWithPassword(string storePath, string password)` (lines 44-49)
---
## Task 4: Remove password-related methods from SecureStoreManager
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/SecureStoreManager.cs`
**Remove these methods:**
- `CreateStoreWithPassword()` (lines 75-96)
- `OpenStoreWithPassword()` (lines 120-140)
---
## Task 5: Add EnsureRequiredKeys method to ISecureStoreManager
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/ISecureStoreManager.cs`
**Add new method:**
```csharp
/// <summary>
/// Ensures all required keys exist in the store, creating blank values for any missing keys.
/// </summary>
/// <param name="requiredKeys">List of keys that must exist.</param>
/// <returns>List of keys that were added.</returns>
IReadOnlyList<string> EnsureRequiredKeys(IEnumerable<string> requiredKeys);
```
---
## Task 6: Implement EnsureRequiredKeys in SecureStoreManager
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/SecureStoreManager.cs`
**Add implementation:**
```csharp
/// <inheritdoc />
public IReadOnlyList<string> EnsureRequiredKeys(IEnumerable<string> requiredKeys)
{
ThrowIfDisposed();
if (_secretsManager == null)
throw new InvalidOperationException("No store is currently open.");
var addedKeys = new List<string>();
foreach (var key in requiredKeys)
{
if (!_keys.Contains(key))
{
_logger.LogInformation("Adding missing required key: {Key}", key);
SetSecret(key, string.Empty);
addedKeys.Add(key);
}
}
if (addedKeys.Count > 0)
{
Save();
}
return addedKeys.AsReadOnly();
}
```
---
## Task 7: Delete locked/unlocked form files
**Delete these files entirely:**
ViewModels:
- `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/SecureStoreLockedFormViewModel.cs`
- `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/SecureStoreUnlockedFormViewModel.cs`
Views:
- `NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreLockedFormView.axaml`
- `NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreLockedFormView.axaml.cs`
- `NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreUnlockedFormView.axaml`
- `NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreUnlockedFormView.axaml.cs`
---
## Task 8: Create SecureStoreInfoFormViewModel
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/SecureStoreInfoFormViewModel.cs` (new file)
**Content:**
```csharp
namespace JdeScoping.ConfigManager.ViewModels.Forms;
/// <summary>
/// View model for the SecureStore info panel shown when the store node is selected.
/// </summary>
public class SecureStoreInfoFormViewModel : ViewModelBase
{
/// <summary>
/// Gets the instruction text to display.
/// </summary>
public string InstructionText => "Select a secret from the tree to view or edit its value.";
/// <summary>
/// Gets the store path for display.
/// </summary>
public string StorePath { get; }
/// <summary>
/// Gets the key file path for display.
/// </summary>
public string KeyFilePath { get; }
/// <summary>
/// Gets the number of secrets in the store.
/// </summary>
public int SecretCount { get; }
public SecureStoreInfoFormViewModel(string storePath, string keyFilePath, int secretCount)
{
StorePath = storePath;
KeyFilePath = keyFilePath;
SecretCount = secretCount;
}
}
```
---
## Task 9: Create SecureStoreInfoFormView
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreInfoFormView.axaml` (new file)
**Content:** Simple panel with instruction text and optional store info display.
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreInfoFormView.axaml.cs` (new file)
**Content:** Code-behind for the view.
---
## Task 10: Update MainWindowViewModel - Remove lock/unlock commands and state
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs`
**Remove:**
- `_openStores` dictionary (line 41)
- `_selectedStoreNode` field (line 42)
- `UnlockStoreCommand` and related methods
- `LockStoreCommand` and related methods
- `RaiseSecureStoreCommandsCanExecuteChanged()` method
- `CreateLockedStoreFormViewModel()` method
- `CreateUnlockedStoreFormViewModel()` method
- `FindParentStoreNode()` method (if only used for lock state)
---
## Task 11: Update MainWindowViewModel - Add auto-initialization logic
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs`
**Add new method `InitializeSecureStoreAsync()`:**
```csharp
/// <summary>
/// Initializes the SecureStore automatically on config load.
/// Creates the store if it doesn't exist and AutoCreateStore is true.
/// Opens the store and ensures all required keys exist.
/// </summary>
private async Task InitializeSecureStoreAsync()
{
if (_appSettings?.SecureStore == null)
return;
if (string.IsNullOrEmpty(ConfigFolderPath) || ConfigFolderPath == "No folder selected")
return;
var secureStoreConfig = _appSettings.SecureStore;
// Resolve paths relative to config folder
var storePath = Path.IsPathRooted(secureStoreConfig.StorePath)
? secureStoreConfig.StorePath
: Path.Combine(ConfigFolderPath, secureStoreConfig.StorePath);
var keyFilePath = Path.IsPathRooted(secureStoreConfig.KeyFilePath)
? secureStoreConfig.KeyFilePath
: Path.Combine(ConfigFolderPath, secureStoreConfig.KeyFilePath);
try
{
if (!File.Exists(storePath))
{
if (!secureStoreConfig.AutoCreateStore)
{
_logger?.LogWarning("SecureStore not found and AutoCreateStore is false");
return;
}
// Create new store with keyfile
_logger?.LogInformation("Creating SecureStore at {StorePath}", storePath);
_secureStoreManager.CreateStore(storePath, keyFilePath);
// Update appsettings.json with actual paths
secureStoreConfig.StorePath = GetRelativePath(ConfigFolderPath, storePath);
secureStoreConfig.KeyFilePath = GetRelativePath(ConfigFolderPath, keyFilePath);
await SaveAppSettingsAsync();
}
else
{
// Open existing store
if (!File.Exists(keyFilePath))
{
await _dialogService!.ShowErrorAsync(
"SecureStore Error",
$"Key file not found: {keyFilePath}\n\nThe SecureStore cannot be opened without its key file.");
return;
}
_secureStoreManager.OpenStore(storePath, keyFilePath);
}
// Ensure all required keys exist
if (secureStoreConfig.RequiredKeys?.Count > 0)
{
var addedKeys = _secureStoreManager.EnsureRequiredKeys(secureStoreConfig.RequiredKeys);
if (addedKeys.Count > 0)
{
_logger?.LogInformation("Added {Count} missing required keys", addedKeys.Count);
}
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to initialize SecureStore");
await _dialogService!.ShowErrorAsync(
"SecureStore Error",
$"Failed to initialize SecureStore:\n\n{ex.Message}");
}
}
private static string GetRelativePath(string basePath, string fullPath)
{
var baseUri = new Uri(basePath.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar);
var fullUri = new Uri(fullPath);
return Uri.UnescapeDataString(baseUri.MakeRelativeUri(fullUri).ToString().Replace('/', Path.DirectorySeparatorChar));
}
```
---
## Task 12: Update MainWindowViewModel - Modify BuildTreeNodes
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs`
**Replace the Secure Stores section in `BuildTreeNodes()` (around lines 464-467):**
**Before:**
```csharp
// Secure Stores folder
var secureStoresFolder = new TreeNodeViewModel("Secure Stores", "🔑", TreeNodeType.SecureStoresFolder) { IsExpanded = true };
LoadConfiguredSecureStore(secureStoresFolder);
TreeNodes.Add(secureStoresFolder);
```
**After:**
```csharp
// Secure Store node (single store, secrets as direct children)
var secureStoreNode = CreateSecureStoreNode();
if (secureStoreNode != null)
{
TreeNodes.Add(secureStoreNode);
}
```
**Add new method `CreateSecureStoreNode()`:**
```csharp
private TreeNodeViewModel? CreateSecureStoreNode()
{
if (_appSettings?.SecureStore == null)
return null;
if (string.IsNullOrEmpty(ConfigFolderPath) || ConfigFolderPath == "No folder selected")
return null;
var storePath = Path.IsPathRooted(_appSettings.SecureStore.StorePath)
? _appSettings.SecureStore.StorePath
: Path.Combine(ConfigFolderPath, _appSettings.SecureStore.StorePath);
var keyFilePath = Path.IsPathRooted(_appSettings.SecureStore.KeyFilePath)
? _appSettings.SecureStore.KeyFilePath
: Path.Combine(ConfigFolderPath, _appSettings.SecureStore.KeyFilePath);
var storeNode = new TreeNodeViewModel("Secure Store", "🔑", TreeNodeType.SecureStore)
{
StorePath = storePath,
KeyFilePath = keyFilePath,
SectionKey = storePath,
IsExpanded = true
};
// Add secrets as direct children if store is open
if (_secureStoreManager.IsStoreOpen)
{
foreach (var key in _secureStoreManager.GetKeys().OrderBy(k => k))
{
var secretNode = new TreeNodeViewModel(key, "🔐", TreeNodeType.Secret)
{
SecretKey = key
};
storeNode.Children.Add(secretNode);
}
}
return storeNode;
}
```
---
## Task 13: Update MainWindowViewModel - Modify OnSelectedNodeChanged
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs`
**Update the switch statement in `OnSelectedNodeChanged()` (around lines 530-557):**
**Remove:**
- `case TreeNodeType.SecureStoresFolder:` block
- Lock/unlock logic in `case TreeNodeType.SecureStore:` block
**Replace with:**
```csharp
case TreeNodeType.SecureStore:
SelectedFormViewModel = CreateSecureStoreInfoFormViewModel();
return;
case TreeNodeType.Secret:
SelectedFormViewModel = CreateSecretFormViewModel(_selectedNode);
return;
```
**Add method:**
```csharp
private SecureStoreInfoFormViewModel CreateSecureStoreInfoFormViewModel()
{
var storePath = _appSettings?.SecureStore?.StorePath ?? "Unknown";
var keyFilePath = _appSettings?.SecureStore?.KeyFilePath ?? "Unknown";
var secretCount = _secureStoreManager.IsStoreOpen ? _secureStoreManager.GetKeys().Count : 0;
return new SecureStoreInfoFormViewModel(storePath, keyFilePath, secretCount);
}
```
---
## Task 14: Update MainWindowViewModel - Call InitializeSecureStoreAsync on load
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs`
**Find the method that loads the config folder (likely `LoadConfigFolderAsync` or similar).**
**Add call to `InitializeSecureStoreAsync()` after loading appsettings.json but before building tree nodes:**
```csharp
// After loading appsettings
_appSettings = await _configFileService.LoadAppSettingsAsync(appSettingsPath, ct);
// Initialize SecureStore (auto-create if needed, open, sync required keys)
await InitializeSecureStoreAsync();
// Then build tree nodes
BuildTreeNodes();
```
---
## Task 15: Remove LoadConfiguredSecureStore method
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs`
**Delete the `LoadConfiguredSecureStore()` method entirely (lines 474-514).**
This is replaced by `CreateSecureStoreNode()` and `InitializeSecureStoreAsync()`.
---
## Task 16: Update DataTemplates for form views
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Views/MainWindow.axaml` (or wherever DataTemplates are defined)
**Remove:**
- DataTemplate for `SecureStoreLockedFormViewModel`
- DataTemplate for `SecureStoreUnlockedFormViewModel`
**Add:**
- DataTemplate for `SecureStoreInfoFormViewModel`
---
## Task 17: Update tests
**File:** `NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/MainWindowViewModelTests.cs`
**Update or remove tests related to:**
- `SecureStoresFolder` node type
- Lock/unlock commands
- Password-based store operations
**Add tests for:**
- Auto-creation of SecureStore on config load
- Sync of missing required keys
- Flattened tree structure
---
## Task 18: Build and verify
```bash
dotnet build NEW/JdeScoping.slnx
dotnet test NEW/JdeScoping.slnx
```
---
## Execution Order
1. Tasks 1-2: TreeNodeViewModel changes (enum, remove lock properties)
2. Tasks 3-6: SecureStoreManager interface and implementation changes
3. Tasks 7-9: Delete old form files, create new info form
4. Tasks 10-15: MainWindowViewModel changes (largest task)
5. Task 16: Update XAML DataTemplates
6. Task 17: Update tests
7. Task 18: Build and verify
---
## Risk Areas
1. **Missing references**: Removing lock/unlock commands may break menu items or toolbar buttons
2. **View DataTemplates**: Must update form selector to use new `SecureStoreInfoFormViewModel`
3. **Test coverage**: Existing tests may expect lock/unlock behavior
## Verification Checklist
- [ ] App builds without errors
- [ ] All tests pass
- [ ] Opening a config folder without SecureStore creates it automatically
- [ ] Opening a config folder with existing SecureStore opens it
- [ ] Missing required keys are added as blank values
- [ ] Tree shows "Secure Store" with secrets directly underneath
- [ ] Clicking "Secure Store" shows instruction panel
- [ ] Clicking a secret shows the secret editor
- [ ] Error dialog shown when keyfile is missing